Andes Script: 
A user manual for knowledge engineers

May 25, 2001


Table of Contents

Overview
The knowledge representation language
    Propositions
    Problem definitions
    Operator definitions
    A long example of how the problem solver really works
    Executable preconditions
The programming environment
    Getting started
    Solving a problem
    Examining the results
 

Overview

Many problems assigned to students in science and engineering courses require them to analyze physical situations.  For instance, a simple physics problem, which will be used through out this document for illustration, is, "If a 50 kg sled slides down a 20 degree frictionless inclined plane for 2 seconds starting from rest, what is its final velocity?"  The analysis consists of a set of primitive equations that can be combined algebraically to produced a value for one or more sought quantities.  Each equation is either justified by a physical principle (e.g., W=m*g, where m is the mass of the sled and W is the magnitude of its weight) or given by the problem (e.g., m=50 kg),   Although the analysis of some problems consists of just one set of primitive equations, other problems can be solved with any of several sets of equations.  For instance, in the sled problem, there are several different solution sets depending on which kinematics equation one uses and which rotations of the coordinate axes one uses.

Andes Script is designed to produce a data structure that represents all possible solutions to a given problem.  The data structure is called a solution graph, and its central component is a particular kind of constraint network [Forbus & de Kleer, 19??, pg. ??] called a bubble graph.   The bubble graph consists of nodes that represent equations and nodes that represent quantities.  If a quantity appears in an equation, then the two nodes are interlinked.  For instance, the simple problem "If what is the mass of a 50 kg block on the moon, where the acceleration due to gravity is 2.78 m/s^2?" has the following nodes in its bubble graph:

1. the mass of the block, m.
2. the magnitude of the weight of the block, W.
3. the accelerational due to gravity on the moon, gm.
4. m=50 kg, the given mass of the block.
5. gm = 2.78 m/s^2, the given acceleration due to gravity on the moon
6. W=m*gm, the weight law, applied to this problem
The links in the graph connect the quantity nodes (1, 2 and 3) with the equation nodes (4, 5, and 6).  In particular, there are links connecting the following pairs of nodes: (4, 1), (5,3), (6,1), (6,2), and (6,3).  Constraint networks such as the bubble graph are a traditional data structure that simplifies a variety of algorithms for processing a system of equations.

Each equation in the solution of a problem must be either given in the problem statement or produced by applying a principle.  To record the justification of equations, each node in the bubble graph has a problem solving method (PSM) graph that represents how the equation was derived.  A solution graph consists of a bubble graph plus a PSM graph for each equation node.

Just as students must know both physics and algebra in order to solve physics problems, the Andes problem solver must have both kinds of knowledge as well.  Its algebraic knowledge is "hard coded," that is, represented in Lisp and not easily changed.  The physics knowledge is "soft coded," that is, represented in a specialized programming language called Andes Script that is designed to make it easy to express such knowledge.

The algebraic code is responsible for constructing the bubble graph, but since it doesn't know anything about which equations are justifiable in this task domain, it calls the physics code to generate equations for it.  In particular, it calls the physics code with the goal, "generate equations that contains <quantity>" where <quantity> is some specific quantity.  For instance, when solving the sled problem, the first call that is made is "generate equations that contain the magnitude of the weight of the sled."  The Andes Script code returns not just one equation, but all equations it can generate that contain the sought quantity.  As the Andes Script code is executed, the interpreter keeps track of its activity and builds a PSM graph to record it.  Thus, what the algebra code actually receives is a set of equation nodes, each containing an equation and a PSM graph representing its justification.

The old Andes problem solver represented its knowledge as if-then rules called "productions", and it used forward chaining to reason.  This meant that goals had to be represented explicitly in the conditions and conclusions of the rules, and the working memory contained propositions representing the goals.  Because the rules could manipulate goals in any fashion they wanted, it was possible to create non-hierarchical goal structures in the solution graphs.  In contrast, Andes Script represents its knowledge as if-then rules called Strips operators [Russell & Norvig, 19??, chapter ??] and it uses backwards chaining to reason. Goals are not explicitly represented in Strips operators, nor do they appear in working memory.  Instead, the interpreter maintains a goal stack.  This means that no matter what code the author writes, the resulting PSM is guaranteed to have a hierarchical goal structure.

Because the operators do not need to manipulate goals, the overall code is somewhat simpler.  This is best appreciated with an illustration, which also helps introduce the coding style.  Consider what the student must do to write a simple scalar equations, such as <speed>=<distance>/<duration>.  The student must define 3 variables then write the equation.  One way to express this reasoning using production rules is to write:

  1. If the goal is to apply the speed-distance-duration equation to ?body from time ?t0 to time ?t1,

  2. then create the goal of defining a variable for the speed of ?body during ?t0 to ?t1,
    create the goal of defining a variable for distance travelled by ?body during ?t0 to ?t1,
    create the goal of defining a variable for duration of ?t0 to ?t1,
    and create the goal of writing the speed-distance-duration equation for ?body from time ?t0 to ?t1.
  3. If the goal is to write the speed-distance-duration equation for ?body from ?t0 to ?t1,

  4. and ?speed is a variable for the speed of ?body during ?t0 to ?t1,
    and ?distance is a variable for the distance traveled by ?body during ?t0 to ?t1,
    and ?duration is a variable for the duration of ?t0 to ?t1,
    then write ?speed = ?distance / ?duration.
  5. If the goal is to define a variable for ?quantity,

  6. and no variable for ?quantity exists in working memory,
    then let ?var be a new variable name appropriate for ?quantity,
    and assert that ?var is a variable for ?quantity.
Production 1 executes, producing 4 subgoals.  Production 3 executes 3 times, defining variable for each of speed, distance and duration.  Then production 2 executes, producing the desired equation.  If variables were defined for some or all of the quantities earlier, then production 3 doesn't have to fire as many times.  Production 2 is happy to use variables defined earlier or newly created variables.  Now to achieve exactly the same effect with the same goal structure, one can write the following Strips operators:
  1. If ?speed is a variable for the speed of ?body during ?t0 to ?t1,

  2. and ?distance is a variable for the distance travelled by ?body during ?t0 to ?t1,
    and ?duration is a variable for the duration of ?t0 to ?t1,
    then write ?speed = ?distance / ?duration,
    which is the speed-distance-duration equation applied to ?body from time ?t0 to ?t1.
  3. If no variable for ?quantity exists in working memory,

  4. then let ?var be a new variable name appropriate for ?quantity,
    and assert that ?var is a variable for ?quantity.
Although these are nearly identical to productions 2 and 3, they are interpreted differently.  Basically, every operator is processed twice, once to produce subgoals (this is called "subgoaling on the operator") and once to produce results (this is called "executing the operator").  Suppose the goal stack initially contains just the goal, "write ?equation, which is the speed-distance-duration equation applied to Nimitz from time 1 to time 2."   The interpreter removes the goal from the stack, finds that operator 1's conclusion matches the goal, and it puts all the operator's conditions on the goal stack followed by a note to itself to apply the operator.  The goal stack is now: In other words, operator 1 has been "processed" once to reduce a goal to subgoals (hence the phrase, "subgoaling on the operator"). Later, when all the subgoals have been achieved, the stack will be just When the interpeter removes the top item from the stack, it sees that it is a note to apply an operator rather than a goal, so puts  the operator's conclusion, namely the speed-distance-duration equation in this case, into working memory.  This is like a second "processing" of the operator (called "executing the operator").    It produces results (additions to working memory) whereas the first "processing" just produced subgoals (addions to the goal stack).  All backchaining reasoners work this way.

Although Prolog is the most widely used commercial backchainer, but we prefer our own version so that we can include some special features.  In particular, an Andes Script operator can have more than just a slot for its conditions and a slot for its conclusions.  It can also have a slot for a hint sequence.  When the help system decides that the student show apply a particular operator, it give the first hint in its hint sequence.  If the student presses the "explain more" button, it will present the next hint in the sequence, and so on.  Thus, the author controls not only what the operators by writing their conditions and conclusions, but how the help system hints about them to the student.

Andes operators also provide a "features" slot, which contains a set of atomic features.  One feature that is particularly important is the "unordered" feature.  When an operator has this feature, then it doesn't matter what order the operators conditions are achieved in.  For instance, operator 1 above should have the "unordered" feature because it doesn't matter which order the student defines variables in.  However, for many operators, order does matter.  For instance, the general procedure for writing a vector equation has the following conditions, which the student achieve in the given order:

If the student enters a compo-free equation before entering a compo equation, then Andes will mark it red and tell the student that the equation is correct but premature.  Although the unordered feature doesn't matter much to the Andes Script interpreter, it controls how the help system interprets the resulting solution graphs.

This illustrates one of the design objectives for Andes Script.  All knowledge about physics should be expressed in Andes Script.  It should not be necessary to modify the Andes interpreter nor the help system in order to extend the task domain or to replace physics knowledge with other domain knowledge.  Part of the knowledge of physics is what steps to do in which order, so that ordering knowledge has to be encoded in Andes Script.  Similarly, the hints depend on the knowledge, so they must be represented in Andes Script.

In order to construct hints and other messages to the student, the help system must refer to quantities and other entities.  Inside the operators, quantities and other entities are represented formally, as logical terms.  For instance, the mass of the Nimitz is represented as (mass Nimitz) and the magnitude of the weight force on it due to the earth is represented as (mag (force Nimitz earth weight)).  Clearly, we do not want to print such expressions in our messages to the student, but converting them into text requires knowledge that is peculiar to physics.  Thus, it must be represented in Andes Script.  Thus, Andes Script has a construction (defexpr??) that indicates how to translate a logical term into English.  It also has a construction (defqexpr) for defining units and constructions for defining other miscelaneous information.  These extra pieces of physics knowledge are called the "ontology."

In addition to operators and defexprs, Andes Script allows authors to define problems.  Thus, the main constructions that the knowledge engineer must write are:

Eventually, we will also let authors define error handlers that can recognize specific errors and handle them in pedagogically appropriate ways.  However, that physics-specific knowledge is currently hard-coded in Lisp.  Realistically speaking, we will probably never be able to completely eliminate domain knowledge from the Lisp and C++ code, but we are trying to minimize it.

Knowledge representation language

Andes Script currently provides ways to define operators, problems and English translations of logical terms.  This section presents the syntax and semantics of problems and operators; the ontology isn't developed enough yet to warrant documentation.

Andes Script is based on several Lisp macros, named defoperator, defproblem, etc.  The files they appear in are regular Lisp code files, which are loaded by the usual Lisp code loading facilities.  In particular, if em1.cl is the name of your Andes Script file, then saying (load "em1") to the top level of Lisp will cause the file to be loaded, which will cause the defoperator, defproblem and defexpr macros to be run, thus causing operators, problems and expressions to be defined.  Like any Lisp file, single line comments are indicated by a semi-colon--the text from there to the end of the line is ignored by the loader.  Multi-line comments begin with #| and end with |#. Emacs, VI and other editors have modes that know about Lisp syntax.  They will balance parentheses for you and color-code the text (e.g., comments are in red, executable code is in black, etc.).

Although one could easily mix defoperators, defproblems and other Andes Script macros in the same file, and one could also define regular Lisp functions in the same file, it is convenient to put all the defoperators in one file, all the defproblems in a second, and all the ontological macros in a third file.  Internally, the macros simply define a Lisp struct (a record instance; like an object with no methods) and add it to a list.  If you execute the same defoperator macro twice, then two copies of the same operator will end up in the list.  Thus, one should put (clear-ops) at the begining of the file that contains the defoperators.  This Lisp function just sets the list of operators to NIL.  Similarly, the file with the problems in it should have (clear-problem-registry) at its beginning and the file with the ontological definitions in it should have (clear-ontology) at its front.
 

Propositions

Andes uses first order logic to represent facts and goals.  Here's a quick review of the essential nomenclature of first order logic.

An atomic statement consists of a predicate followed by its arguments.  For instance, (massless sled) is a statement that the sled is massless.  "Massless" is the predicate, and "sled" is a constant that denotes a particular sled.  Predicate names and constants are just Lisp atoms.

The arguments of a predicate can also be a variable.  Unfortunately, Lisp also uses the term "variable" to mean something different, so we use "unification variable" to mean the kinds of variables that appear in logical statements.  Unification variables always begin with a question mark.  For instance,  the proposition (tied-to ?string ?body) could be used as a goal that means "find out if something is tied to something."

The arguments of a predicate can also be a function, where a function consists of a function name and its arguments.  For instance, the proposition (given (duration (during 1 2)) (dnum 13656 |s|)) says that the duration from time 1 to time 2 is 13656 seconds.  The function (during 1 2) denotes a time interval.  The function (duration (during 1 2)) denotes a quantity, namely the duration of that time interval.  The function (dnum 13656 |s|) denotes a dimensional number.  13656 is a constant that denotes a number.  |s| is a constant that denotes a unit, namely seconds.  The vertical bars here are necessary to make this atom be spelled in lower case.  Lisp automatically puts atoms that are not surrounded by vertical bars into upper case.

There is much more to first order logic, but it isn't used in Andes.  In particular, the knowledge base does not use quantifiers, conjunctions, disjunctions, negations, implications, etc.

Problem definitions

A problem is defined with the defproblem macro, which has the following syntax:
(defproblem <name>
    :statement <list of strings>
    :features <list of Lisp forms>
    :givens <list of propositions>
    :soughts <list of propositions>)
Here is an example of a problem definition
(defproblem Exkt1a
    :statement (
"The SR-71 strategic reconnaissance aircraft, the Blackbird, set a world speed"
"record by flying from London to Los Angeles (8.79x10^6 m) in 3 hours 47 minutes"
"and 36 seconds. Compute the average speed in m/s." )
    :features (working clips kinematics)
    :givens
       ((object aircraft)
        (time (during 1 2))
        (given (duration (during 1 2)) (dnum 13656 |s|))
        (given (at (distance aircraft) (during 1 2)) (dnum 8790000 |m|)))
   :soughts ( (answer (at (speed aircraft) (during 1 2))) ))
The <name> is just a Lisp atom that names the problem.  The rest of the macro is a set of fields, each preceeded by a label (which always starts with colon).  Because the fields are labelled, they may appear in any order.

The statement field contains a list of strings.  It should contain the exact wording form the Andes screen, but it doesn't matter how it is broken up into strings.  Currently, the system pays no attention to the contents of this field, because the wording that is painted on the Andes screen comes from other files.  However, that should eventually change, and the Workbench will display the contents of this field.

The features field contains a list of Lisp atoms.  These are conveniences for the author.  For instance, the author can run a tool that collects all problems that have the "working" feature and submits them to the problem solver.  This allows an author to rapidly check that a change to the KB has not introduced any bugs into previously working problems.

The givens field is a list of propositions that represent the information given in the problem statement. The propositions are ground atomic statements in first order logic.  That is, they have no variables, no quantifiers and no logical connectives (e.g., conjunction, disjunction or negation).

The soughts field is a list of propositions that need to be derived.   In the current problems, this is just a list of the quantities sought by the problem, each one inclosed in an (answer …) predicate.

Operator definitions

An operator is defined with the defoperator macro, which has the following syntax:
(defoperator <name> <arguments>
   :specifications <string>
   :features <list of features>
   :preconditions <list of conditions>
   :effects <list of effects>
   :hint <list of hints>)
For example, here is an operator that draws a weight vector:
(defoperator draw-weight (?b ?t ?planet)
  :specifications "
    If ?b is not massless, and
       it is near a ?planet,
    then draw a weight vector for it pointing straight down,
       define a magnitude variable and an direction variable for it."
  :preconditions
   ((force ?b ?planet weight ?t ?dir action)
    (not (vector ?b (at (force ?b ?planet weight) ?t) ?dont-care))
    (bind ?mag-var (format-sym "Fw_~A_~A_~A" (body-name ?b) ?planet
                                             (time-abbrev ?t)))
    (bind ?dir-var (format-sym "O~A" ?mag-var))
    (debug "~&Drawing weight of ~a at ~a.~%" ?b ?t))
  :effects
   ((vector ?b (at (force ?b ?planet weight) ?t) ?dir)
    (variable ?mag-var (at (mag (force ?b ?planet weight)) ?t))
    (variable ?dir-var (at (dir (force ?b ?planet weight)) ?t))
    (given (at (dir (force ?b ?planet weight)) ?t) ?dir))
  :features ()
  :hint
   ((hint "Notice that ~a is near ~a." ?b ?planet)
    (hint "When an object is near a planet, the planet exerts a
           weight force on the object.")
    (hint "Because ~a is near the planet ~a, the planet exerts a weight
           force on it, so use the force drawing tool to draw a force on
           ~a due to ~a of type weight during time ~a." ?b ?planet ?b ?planet ?t)))
The <name> field is a Lisp atom, such as draw-weight.  The <arguments> field is a list of unification variables, where a unification variable is a Lisp atom that starts with a question mark.  Unification variables are used in many places in Andes Script.  Unification is a kind of pattern matching.  When a form containing a unification variable, such as (mass ?body), is unified (matched) to another form, such as (mass sled), the unification variables are bound to the corresponding pieces, so ?body is bound to sled.

The arguments of an operator are used to prevent redundant computation.  Whenever an operator is applied, its name and the bindings of its arguments are stored, and the interpreter will not apply an operator if it has already been applied with the same argument bindings.  Thus, the author should include in the argument list enough variables to insure uniqueness.  For instance, if ?t were left out the argument list of draw-weight, then the interpreter would refuse to draw the weight of the Nimitz (?b=Nimitz) due to the earth (?planet=earth) for time point 1  (?t=1) if it had already drawn the weight of the Nimitz due to earth for time point 2.

Although the <name> and <arguments> fields are required, the rest of the operator's fields are not.  Thus, we need to label them in order to tell which ones are present and which have been left out.  The labels for the fields begin with a colon.  These fields can appear in any order, since the labels suffice to distinguish them.

The specifications field is a string used only for documentation.  So far, we haven't had much use for it.  It usually takes a couple of paragraphs to explain an operator, so authors have been putting those explanations in Lisp comments.

The preconditions field is an ordered list of conditions.   A condition can be either an atomic statement or an executable.  An executable is a piece of lisp code; the list of valid executables will be discussed later.  A atomic statement is a first-order logic statement, written in Lisp syntax, with no quantifiers and no logical connectives.  That is, it consists simply of a predicate and arguments, where each argument is a logical term.  A logical term is either a unification variable (e.g., ?planet), a constant (e.g., earth) or a logical function.  A logical function is a function name with a list of arguments.   For instance, (force ?b ?planet weight) is a logical function, and (vector ?b (at (force ?b ?planet weight) ?t) ?dir) is an atomic statement.  Logical functions and atomic statements look exactly the same, syntactically.  However, they appear in different places.  Atomic statements appear as conditions and effects.  Functions appear in the argument positions of atomic statements or other functions.  This is just the standard conventions of first order logic.   When an atomic statement appears in the preconditions field of an operator, it is used both to unify with working memory and as a subgoal.  This was illustrated earlier when the differences between productions and Stripts operators was discussed.

The effects field is an unordered list of atomic statements.  These will eventually be added to working memory when the operator is finally executed.

The features field contains a list of Lisp forms.  Currently, the only feature is unordered.  When an operator has the unordered feature, it means that the preconditions of the operator can be achieved by the student in any order.  The interpreter will continue to achieve them in order, so the unordered feature has no affect on the solution graph generator.

The hint field contains a list of hints to be given to the student.  This field's contents are not yet used by the help system, so it won't be documented any further.  We might end up having to change the syntax of hints as the implementation of the help system proceeds.

The fields of operators have been described, except that we delayed discussion of the executables that appear in the preconditions slot.  They are needed in order to escape from the ordinary interpretation of operators.  Thus, we need to describe more precisely what the interpreter does before describing the executables.

A long example of how the problem solver really works

When the bubble graph generator wants to receive all possible equations that contain a specific sought quantity, it calls the interpreter and passes in the sought quantity.  Suppose, for instance, that it wants to find equations that contain the mass of the sled, so it passes in (mass sled).  The interpreter initializes working memory with propositions from the problem definition and it initializes the goal stack with the goal This is not actually what happens, but it is pretty close (see the operator find-by-psm in Newtons2.cl for the real truth).  At any rate, the key idea is that problem solving starts with a non-empty working memory and a goal stack that has just one goal on it.

However, the bubble graph should receive not just one equation, but all possible equations that contain the sought quantity.  This is achieved by making the interpreter non-deterministic.  That is, the whole state of the interpreter is recorded in a data structure called the State.  In particular, the State contains the working memory, the goal stack, and the bindings of any unification variables.  The interpreter's has a workhorse function, called Successors, that inputs a State and outputs all possible successor states.  If any of those States has an empty goal stack, then the State represents a solution to the problem so it is put aside.  Otherwise, the remaining States are placed in a queue.  The interpreter repeatedly pulls a State off the queue, calls Successors to generates all its successors, puts aside the completed states, and enqueues the rest.  When the queue is finally empty, the interpreter has generated all possible solutions.  It extracts the equations from them, and returns them to the bubble graph generator.

Successors inputs one State and outputs a possibly empty set of States.  The basic steps are:

  1. Remove the top item from the goal stack of the input State.
  2. If the top item is an operator, then execute it.  That is, create a new state that is a copy of the input State, then add the operator's effects to its working memory, after first replacing any unification variables that appear in the effects with their current bindings.  Return a singleton set containing the new State.
  3. If the top item is a executable, then create a new state that is a copy of the input State, execute the executable in the new state, and and return a singleton set constaining the new state.
  4. Otherwise, the top item must be an atomic statement (i.e., a goal).  Do the next two steps in order to process it.  They will produce zero or more new states.  Return a set containing the new states.
    1. For each proposition in working memory that unifies with the goal, create a new state with the bindings produced by the unification.  In other words, the goal was already true in working memory, so we don't have to do anything, except remember what bindings were produced by matching the goal to working memory.
    2. For each operator in the knowledge base that has an effect that unifies with the goal, create a new State by subgoaling on the operator.  That is, push the operator onto the new State's goal stack, then push all the preconditions onto the goal stack.
This will probably be clearer if we work through an example.  Suppose the initial State has the sled problem's description in its working memory and has (psm-applied (mass sled) ?eqn-id ?eqn-algebra) as the only item on its goal stack.  Successors inputs the initial State, and removes (psm-applied (mass sled) ?eqn-id ?eqn-algebra) from the stack.  This is an atomic statement, so step 4 applies.  First Successors tries to unify it with propositions in working memory, but no propositions match.  Next Successors tries to unify the goal with each operator's effects, and unification succeeds with the following operator:

;;; =================== applying a scalar equation=================
;;; This operator is a problem solving method (PSM) that finds a
;;; scalar equation containing the sought quantity then writes the
;;; equation.  Whenever an author wants to define a new scalar
;;; equation, such as speed=distance/duration, the author must define
;;; operators for both the eqn-contains goal, which indicate which
;;; quantities are contained in the equation, and the eqn goal, which
;;; represents writing the equation on the Andes user interface.

(defoperator apply-scalar-psm (?sought ?eqn-id)
   :features (PSM)
   :specifications "If the goal is to apply a psm to find a quantity,
      and there is a scalar equation  that contains that quantity,
      then generate the equation."
   :preconditions (
      (eqn-contains ?eqn-id ?sought)
      (not (eqn ?dont-care ?eqn-id))
      (eqn ?eqn-algebra ?eqn-id)
   )
   :effects
   ((psm-applied ?sought ?eqn-id ?eqn-algebra)))

Successors subgoals on this operator and creates a new State with the following goal stack

No other operator has an effect that unifies with the goal, so step 4 of Successors finishes up having created just one State.  The State is returned, enqued, immediately dequeued, and given back to Successors.  Successors pops the stack, and identifies (eqn-contains ?eqn-id (mass sled)) as an atomic statement and hence a goal.  There are no matching propositions for it in the working memory.   Several operators have effects that unify with the goal.  Let's look first at this one:

(defoperator wt-law-contains (?b ?t ?planet)
  :specifications "
   If a body is near a planet,
   then the weight law for the body potentially contains
     the magnitude of the weight force,
     the mass of the body, and
     the gravitational constant for the planet."
  :preconditions
  ((time ?t)
   (near-planet ?planet)
   (not (massless ?b)))
  :effects
  ((eqn-contains (wt-law ?b ?t) (mass ?b))
   (eqn-contains (wt-law ?b ?t) (at (mag (force ?b ?planet weight)) ?t))
   (eqn-contains (wt-law ?b ?t) (gravitational-acceleration ?planet)))
  :hint
  ((hint "The weight law could potentially contain the sought quantity.")))

The first effect unifies with the goal, and binds ?eqn-id to (wt-law sled ?t) as a side effect.  Successors creates a new State and puts this operator's conditions, etc. onto its goal stack, which contains:

Note that these little pictures of the goal stack show the bindings of unification variables.  Thus, instead of (eqn ?eqn-algebra ?eqn-id), we see (eqn ?eqn-algebra (wt-law sled ?t)) because ?eqn-id has been bound to (wt-law sled ?t) .  At any rate, this State is one of the states returned by Successors.  However, there are other operators with an effect that unifies with the goal.  Here is one:

(defoperator mass-compound-contains (?b-sought ?bodies)
  :preconditions (
   ; compound body must exist
   (object (compound . ?bodies))
   ; applies if sought is mass of compound or one of its parts
   (test (or (member ?b-sought ?bodies)
             (equal ?b-sought `(compound ,@?bodies))))
  )
  :effects (
    (eqn-contains (mass-compound ?bodies) (mass ?b-sought))
  ))

Subgoaling on this operator produces a state that has the following goal stack:

Successors returns a set of States that includes this state, the State generate by back chaining on the weight law, and other States generated by back chaining on conservation of kinetic energy and a variety of moment of inertia equations.  In other words, the procedure returns a set of States, one for each scalar equation in the knowledge base that contains mass.  Each state has a goal stack that contains conditions that need to be tested in order to determine whether the equation applies.

All these States are placed in the queue.  Each will be followed out to see if eventually it eventually leads to an equation.  Let's first consider the fate of the State for the weight law.  The top item on its goal stack is (time ?t).  At step 4 of Successors, we find that (time ?t) unifies with the proposition (time (during 1 2)) in working memory.  A new State is created with ?t bound to (during 1 2) and the following goal stack:

Note how all the ?t variables in the goal stack have changed to (during 1 2).  Successors next searches for an operator with an effect that unifies with (time ?t), but fails to find any, so it returns only the one State.  Let's suppose that this state is enqueued, immediately dequeued and send back to Successors.  Now the top item on the goal stack is (near-planet ?planet).  This unifies with the proposition (near-planet earth) in working memory, and a new State is produced with a shorter goal stack and ?planet bound to earth: No operators have near-planet propostions in their effects, so this is the only State produced.  Let us again suppose that it is enqueued and immediately dequeued.  Now the top item on the stack is (not (massless sled)).  This is an executable, so Successors follows its third step and calls the Lisp code associated with the executable.  The code checks whether (massless sled) appears in working memory.  It does not, so (not (massless sled)) succeeds.  A new State is created with the following goal stack: Suppose this State is enqueued, immediately dequeued, and sent back to Successors.  Successors recognizes the top item on the goal stack as an operator application, so it executes the operator.  That is, it creates a new State and addes the operators effects to the state's working memory.  The working memory becomes: The first three propositions were just added; the others are given in the statement of the problem, "What is the final velocity of a 50 kg sled that starts at rest and slides down a 20 degree frictionless inclined plane for 2 seconds?"  The goal stack of the new State is: At this point, let's speed up time and get to the end of this long example.  The top goal is an executable that checks where this equation has been written already, and it hasn't.  The next goal is to generate the wt-law equation for the sled over the given time interval.  This is accomplished by first subgoaling on the operator:

(defoperator wt-law (?b ?t)
  :features (unordered)
  :specifications "
   If a body is near a planet,
     and it is not massless,
     and you can find the appropriate variables,
   then write W=m*g where W is the magnitude of the weight force
     on the body, m is the body's mass and g is the gravitational
     acceleration of the planet."
  :preconditions
   ((near-planet ?planet)
    (not (massless ?b))
    (variable ?m-var (mass ?b))
    (variable ?w-var (at (mag (force ?b ?planet weight)) ?t))
    (variable ?g-var (gravitational-acceleration ?planet))
    )
  :effects
   ((eqn (= ?w-var (* ?m-var ?g-var)) (wt-law ?b ?t)))
  :hint
   ((hint "You should try applying the weight law.")
    (hint "The weight law for a body is W=m*g, where W is the magnitude of the
           weight force acting on the body, m is the body's mass and g is the
           gravitational acceleration of earth or whatever planet is nearby.")
    (hint "Write ~a=~a*~a" ?w-var ?m-var ?g-var)))

In order to satisfy the third and fourth preconditions, operators are applied that draw the body and the weight.  Another operator is applied to represent the fact that "g" is predefined as the variable for gravitational acceleration.  When all 3 variables are defined, the wt-law operator is executed and writes an equation into working memory.  This allows apply-scalar-psm to execute, thus creating a State with an empty goal stack.  The interpreter puts this State aside as one solution to the problem.  It dequeues the State generated by subgoaling on the mass-compound-contains operator.  The State has this goal stack:

The top goal cannot be achieved, so Successors eventually returns a null set.  Now the only states left on the queue were created by subgoaling on conservation of kinetic energy and by various moment of inertial operators.  Their preconditions can't be achieved either, so eventually Successors returns null sets for them as well.  Thus, the queue eventually becomes empty, and the interpreter stops.  Only one successful State has been created, namely the one for the weight law, so just that equation is returned to the bubble graph generator.  However, attached to it is a PSM graph, which looks roughly like: This illustrates that the PSM graph is basically just the sequence of reasoning steps taken by Successors.  Although an author could read the PSM graph in order find bugs in the KB, it is easier to use tracing tools instead.  Thus, the PSM graphs will not be explained any further here.

Executable preconditions

THis section documents the executables that can occur in the preconditions field of operator definitions.  Whenever it says "the <Lisp form> is executed," it means that bindings for unification variables are first substituted into the form and then it is executed.  Thus, unification variables act like Lisp variables in this context.

(bind <unification variable> <Lisp form>)  This causes the <Lisp form> to be executed.  The value it returns is bound to the <unification variable>.  If the <unification variable> is already bound, then its binding must equalp the value returned by Lisp.   That is, this executable is like an assignment statement in a programming language.  For instance,  (bind ?dir-var (format-sym "O~A" ?mag-var)) calls the Lisp function format-sym and passes in both the string "O~A" and the value of a unification variable (which that happens to be the variable for the magnitude of a vector).  The format-sym function, which acts like a Lisp format statement except that it creates symbols instead of printing to an output stream, returns a symbol that has "O" on the front of the magnitude variable's symbol.  The unification variable ?dir-var is bound to this newly created symbol.

(test <Lisp form>) This causes the <Lisp form> to be executed.  If it returns NIL, the precondition fails.  If it returns anything else, the precondition succeeds.  For instance, (test (non-zero-projectionp ?rot ?dir)) calls a Lisp function that checks whether projecting a vector with direction ?dir onto an axis with rotation ?rot will result in a non-zero component.

(not <atomic statement>)  If any proposition in working memory unifies with the given atomic statement, this precondition fails.  It succeeds only if there is no proposition in working memory that so unifies with the given statement.  For instance, (not (vector ?b (at (force ?b ?planet weight) ?t) ?dont-care)) succeeds only if there is no weight vector in working memory for the given body, planet and time.

(not <atomic statement> <Lisp form>) This fails there is a proposition in working memory that unifies with the given atomic statement and the Lisp form returns non-NIL when executed.  For instance, (not (axis-for ?sys ?t ?dontcare1 ?dontcare2) (part-of-sys ?b ?sys)) fails if an axis has been drawn for a system ?sys and the body ?b is a part of ?sys.  Part-of-sys is a Lisp function.

(in-wm <atomic statement>)  For each proposition in working memory that unifies with the given atomic statement, this precondition succeeds and Successors returns a State.  Moreover,  the bindings made during unification are kept in that State.  This is just like putting the atomic statement into the precondition except that Successors will not try to unify it with the effects of operators.  That is, it will match the statement to working memory but it will not try to find an operator that will achieve it.  If there are no propositions in working memory that unify with the atomic statement, then Successors returns no States.  That is, the precondition fails.

(count <unification variable> <atomic statement>)  This counts the number of propositions in working memory that unify the with the given atomic statement, and binds the given unification variable to that integer.

(add-to-wm <atomic statement>)  This just adds the given <atomic statement> to working memory, after first substituting bindings for any variables that occur in the statement.  Thus, if ?b is sled, then (add-to-wm (teleport ?b)) would add (teleport sled) to working memory.

(any-member <unification variable> <list of terms>)  After values are substituted for any unification variables, <set of terms> must be a possibly empty list (it is an error if it does not).  For each unification of the unification variable with a member of this list, a new state is created and returned by Successors.  For instance, (any-member ?x-rotation ?min-dirs) is used inside the axis rotation operator draw-rotate-axes.  The variable ?min-dirs is bound to a list of directions.   This precondition causes Successors to returns one state for each direction, with the variable ?x-rotation is bound to the selected direction.

(debug <string> . <list of terms>)  This is like a Lisp format statement.  The string is printed except that special format characters, such as ~a, are replaced by values from the list of terms.  For instance, (debug "angle between ~A and ~A = ~A~%" ?v1-var ?v2-var ?angle) prints out the angle between two quantities.  The ~% causes a line break to be printed.  This printing can be suppressed by setting the global Lisp variable *debug* to NIL.

(setof  <goal> <term> <unification variables>)  This is best explained with an example:
     (setof (vector ?b (at (force ?b ?agent ?type) ?dir)
            (force ?b ?agent ?type)
            ?forces)
This executable starts by finding all possible derivations of <goal>, namely (vector ?b ...?dir).  That is, Successors is called repeatedly with the <goal> on the top of the stack until no new States are produced.  In this case, that derives all forces acting on the given body ?b, because the other variables are unbound at the time this precondition is executed.  Once all the States have been collected, the <goal> is unified with either of their working memories in order to get bindings for its variables.  These bindings are applied to the <term>, creating a different term for each state.  That set of terms is bound to the given <unification variable>  For instance, if since there are two forces on the sled, two States are produced.  One has derived (vector sled (t (force sled earth weight) (dnum 270 |deg|)) and the other has derived (vector sled (force sled plane normal) (dnum 20 |deg|)).  From these two States, the force terms are extracted, and ?forces is bound to ((force sled earth weight)(force sled plane normal)).

(foreach <unification variable> <Lisp form> <atomic statement>)  This executable is actually a macro.  It expands into a list of preconditions that replace it.  For instance, suppose that
    (foreach ?b ?bodies (vector ?b (at (velocity ?b) ?t1) ?dir1))
is executed when ?bodies is bound to (ball bat) and ?t1 is bound to 2.  Execution causes this precondition to be replaced by two preconditions:
    (vector ball (at (velocity ball) 2) ?g007181)
    (vector ball (at (velocity bat) 2) ?g007182)
In general, the <Lisp form> should return a list of terms when it is executed.  For each member of this set, the <unification variable> is bound to the member, and a copy of the <atomic statement> is turned into a precondition.  Any variables in the <atomic statement> that are unbound when this occurred are turned into variables that are guaranteed to be unique.

(map <unification variable> <Lisp form> <atomic statement> <unification variable> <unification variable>)  This is also a macro and expands into a list of preconditions that replace it.  For instance, suppose we execute:
   (map ?v
        (vars-in-eqn ?eqn-algebra)
        (in-wm (variable ?v ?q))
        ?q
        ?quantities-in-eqn)
when ?eqn-algebra is bound to (= w (* m a)).  It expands into
    (in-wm (variable w ?q1))
    (in-wm (variable m ?q2))1
    (in-wm (variable a ?q3))
    (bind ?quantities-in-eqn '(q1 q2 q3))
That is, the interpreter calls the <Lisp form> and expects to receive a list back.  In this case, it calls vars-in-eqn which extracts variables from (= w (* m a)) and returns the list (w m a).  For each member of this list, the first <unification variable>, ?v in this case, is bound to the member and a copy of <goal>,  (in-wm (variable ?v ?q)) in this case, is entered as a precondition.  The the unbound variables (only ?q in this case) are renamed to make them unique.  Lastly, the renamed variables that correspond to the second <unification variable>, ?q in this case, are gathered together into a list and included in a bind executable that binds them to the third <unification variable>, ?quantities-in-eqn in this case.

The Andes Script programming environment

This section describes how to load and use the current system.  If you just want to read the code, here are the relevant files: The ontology.cl file isn't ready yet.  It only has a few uninteresting definitions in it.

Getting started

These instructions assume you have stored the files into C:/Andes2/.  To do this from the zip utility, extract the files into C:/ and be sure to check the option to user folder names.  This will create C:/Andes2 and its subdirectories.

Start Lisp in your favorite working envirnoment.  If you are using the IDE that comes with Lisp, start by executing (in-package :user).  If you are using Emacs or the Lisp console interface, this is unnecessary.

Set the working directory to the one that contains the Andes2 files by executing :cd C:/Andes2

Next load the system loader file by executing :ld sgg/loader. You could also do (load "sgg/loader").

Next, recompile and reload all the files (for speed) by executing (rca).
 

Solving a problem

To prevent being overwhelmed with output, turn off some of the tracing by executing (setq *debug* NIL) and (setq *S-print-steps* NIL).  Check that you got the switches turned off by executing (sw), which prints a table of switches and their current settings.

Next, try solving a problem, such as exkt1a by executing (s exkt1a).  Here's the call and the output, with annotations in variable width font explaining what the output means.

USER(22): (s exkt1a)
1 1111111111111111 1  Generating Bubblegraph: EXKT1A  1 1111111111111111 1
WARNING: PSM Subevars differ;
 ((VARIABLE m_AIRCRAFT (MASS AIRCRAFT)))
 NIL
2 2222222222222222 2  Generating Equation Index: EXKT1A  2 2222222222222222 2
3 3333333333333333 3  Generating Var Index: EXKT1A  3 3333333333333333 3
4 444444444444444444 4  Generating Solution Point: EXKT1A  4 444444444444444444 4
Complete Solution Point Returned.
5 555555555555555555 5  Generating Solution Bubbles: EXKT1A  5 555555555555555555 5
6 666666666666666666 6  Marking Problem Graph: EXKT1A  6 666666666666666666 6
NIL
[2] USER(23):

The (s...) function creates the whole solution graph in 6 stages.  It first stage creates the bubble graph.  This is where the KB code is executed.  A warning message was generated here (you can ignore this).   Next it generates two indices needed by the help system.  The fourth stage is to submit the equations to the algebraic equation solver and make sure that they are solvable.  In this case, one solution point was generated, which indicates success.  Next it calculates all possible paths through the bubble graph.  Each path corresponds to a solution path that the student could take, so these are sometimes interesting to look at in order to debug the KB code.  In the sixth stage, some bookkeeping is done that involves marking parts of the solution graph for the sake of the help system.

When working on extremely simple problems, it is sometimes useful to trace the reasoning of the problem solver as it occurs.  Executing (setq *actions* T) will cause a complete trace of the reasoning to be printed.  Executing (setq *debug* T) will cause the trace statements in the Debug executables to be printed.

Examining the solution graph

At this point, the solution graph is stored in Lisp's memory and can still be examined in various ways.  There are many functions for doing this.  Executing (pg) shows a short form of the bubble graph:

===== Quantity nodes: =====
<Quantity-Node: 0 2 (AT (SPEED AIRCRAFT) (DURING 1 2))
  Variable: sp_AIRCRAFT_12
  Equations: (<Eq: (SDD AIRCRAFT (DURING 1 2))>)
  Marks: (SOUGHT)
  Path: NIL>

<Quantity-Node: 1 1 (AT (DISTANCE AIRCRAFT) (DURING 1 2))
  Variable: dist_AIRCRAFT_12
  Equations: (<Eq: (SDD AIRCRAFT (DURING 1 2))>
              <Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2))
                    (DNUM 8790000 m))>)
  Marks: (GIVEN)
  Path: NIL>

<Quantity-Node: 2 0 (DURATION (DURING 1 2))
  Variable: t_12
  Equations: (<Eq: (SDD AIRCRAFT (DURING 1 2))>
              <Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>)
  Marks: (GIVEN)
  Path: NIL>
 

===== Equation nodes: =====
<Equation-Node: 3 2 (SDD AIRCRAFT (DURING 1 2))
  Algebra: (= sp_AIRCRAFT_12 (/ dist_AIRCRAFT_12 t_12))
  Quantities: (<Q: (AT (SPEED AIRCRAFT) (DURING 1 2))>
               <Q: (AT (DISTANCE AIRCRAFT) (DURING 1 2))>
               <Q: (DURATION (DURING 1 2))>)
  Marks: NIL

<Equation-Node: 4 1 (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2))
                     (DNUM 8790000 m))
  Algebra: (= dist_AIRCRAFT_12 (DNUM 8790000 m))
  Quantities: (<Q: (AT (DISTANCE AIRCRAFT) (DURING 1 2))>)
  Marks: (GIVEN)

<Equation-Node: 5 0 (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))
  Algebra: (= t_12 (DNUM 13656 s))
  Quantities: (<Q: (DURATION (DURING 1 2))>)
  Marks: (GIVEN)

The Path fields and the marks can be ignored here.  If you want to see the PSM graph associated with a node, execute (pgn <node number>) where the node number is the number immediately following the ...-Node: above.  For instance, (pgn 5) prints the details about the last node listed above:

USER(27): (pgn 5)
<Equation-Node: 5 0 (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))
  Algebra: (= t_12 (DNUM 13656 s))
  Path: ((SG DUMMY (GIVEN-EQN ?EQN (DURATION (DURING 1 2))))
         (OP (WRITE-KNOWN-VALUE-EQN (DURATION (DURING 1 2))))
         (SG WRITE-KNOWN-VALUE-EQN
          (IN-WM (GIVEN (DURATION (DURING 1 2)) ?VALUE-EXPR40340)))
         (SG WRITE-KNOWN-VALUE-EQN
          (TEST (OR (NUMBERP (DNUM 13656 s)) (LISTP (DNUM 13656 s)))))
         (SG WRITE-KNOWN-VALUE-EQN
          (VARIABLE ?VAR-NAME40339 (DURATION (DURING 1 2))))
         (OP (DEFINE-DURATION (DURING 1 2)))
         (SG DEFINE-DURATION (TIME (DURING 1 2))) (WM (TIME (DURING 1 2)))
         (SG DEFINE-DURATION (TEST (TIME-INTERVALP (DURING 1 2))))
         (SG DEFINE-DURATION
          (NOT (VARIABLE ?DONT-CARE40343 (DURATION (DURING 1 2)))))
         (SG DEFINE-DURATION
          (BIND ?VAR40344 (FORMAT-SYM t_~A (TIME-ABBREV (DURING 1 2)))))
         (DO (DEFINE-DURATION (DURING 1 2))
             ((DEFINE-VAR (DURATION (DURING 1 2)))
              (VARIABLE t_12 (DURATION (DURING 1 2))))
           (?DONT-CARE40343 t_12 (DURING 1 2)))
         (DO (WRITE-KNOWN-VALUE-EQN (DURATION (DURING 1 2)))
             ((GIVEN-EQN (= t_12 (DNUM 13656 s)) (DURATION (DURING 1 2))))
           (t_12 (DNUM 13656 s) (DURATION (DURING 1 2))))
         (DO (DUMMY) ()  NIL))
  Assumptions: NIL
  Quantities: (<Q: (DURATION (DURING 1 2))>)
  Marks: (GIVEN)
  State: NIL
  Subeqns: ((EQN 0 GIVEN-EQN (= t_12 (DNUM 13656 s)) (DURATION (DURING 1 2))
             (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>) T))
  Subvars: NIL
  Entries: NIL>

The Path field shows all the reasoning steps the problem solver went through to generate this equation node.  This is often too much information to be helpful.

A more helpful printout is (pe), which prints all possible paths through the solution graph.  These are the path that the student must actually go through, so sometime it is useful to study them.  In this case there is just one path:

USER(28): (pe)
Equation sets for problem: EXKT1A
0: (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>
    <Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2)) (DNUM 8790000 m))>
    <Eq: (SDD AIRCRAFT (DURING 1 2))>)
 (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>
  <Q: (DURATION (DURING 1 2))>
  <Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2)) (DNUM 8790000 m))>
  <Q: (AT (DISTANCE AIRCRAFT) (DURING 1 2))> <Eq: (SDD AIRCRAFT (DURING 1 2))>
  <Q: (AT (SPEED AIRCRAFT) (DURING 1 2))>)
 NIL

To see the details about one path, execute (pep <path number>), so (pep 0) gives:

USER(29): (pep 0)
0: (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>
    <Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2)) (DNUM 8790000 m))>
    <Eq: (SDD AIRCRAFT (DURING 1 2))>)
 (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>
  <Q: (DURATION (DURING 1 2))>
  <Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2)) (DNUM 8790000 m))>
  <Q: (AT (DISTANCE AIRCRAFT) (DURING 1 2))> <Eq: (SDD AIRCRAFT (DURING 1 2))>
  <Q: (AT (SPEED AIRCRAFT) (DURING 1 2))>)
 NIL

<B: Knowns:      (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>
                  <Q: (DURATION (DURING 1 2))>
                  <Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2))
                        (DNUM 8790000 m))>
                  <Q: (AT (DISTANCE AIRCRAFT) (DURING 1 2))>
                  <Eq: (SDD AIRCRAFT (DURING 1 2))>
                  <Q: (AT (SPEED AIRCRAFT) (DURING 1 2))>)
    NIL
    Soughts:    NIL>

This prints the path in increasing detail: first the equations nodes in this path, then all nodes in this path and finally the path itself.  To print out the list of variables, execute (pgv).

USER(34): (pgv)

((QVAR 0 t_12 (DURATION (DURING 1 2)) 13656 s (POSITIVE)
  (<Q: (DURATION (DURING 1 2))>))
 (QVAR 1 dist_AIRCRAFT_12 (AT (DISTANCE AIRCRAFT) (DURING 1 2)) 8790000 m NIL
  (<Q: (AT (DISTANCE AIRCRAFT) (DURING 1 2))>))
 (QVAR 2 sp_AIRCRAFT_12 (AT (SPEED AIRCRAFT) (DURING 1 2)) 643.67311072056 m/s
  NIL (<Q: (AT (SPEED AIRCRAFT) (DURING 1 2))>)))

This is a list of 3 descriptors.  Each one has the following items:

To view the equations, execute (pge):

USER(35): (pge)

((EQN 0 GIVEN-EQN (= t_12 (DNUM 13656 s)) (DURATION (DURING 1 2))
  (<Eq: (GIVEN (DURATION (DURING 1 2)) (DNUM 13656 s))>) T)
 (EQN 1 GIVEN-EQN (= dist_AIRCRAFT_12 (DNUM 8790000 m))
  (AT (DISTANCE AIRCRAFT) (DURING 1 2))
  (<Eq: (GIVEN (AT (DISTANCE AIRCRAFT) (DURING 1 2)) (DNUM 8790000 m))>) T)
 (EQN 2 EQN (= sp_AIRCRAFT_12 (/ dist_AIRCRAFT_12 t_12))
  (SDD AIRCRAFT (DURING 1 2)) (<Eq: (SDD AIRCRAFT (DURING 1 2))>) T))

This is also a list of 3 descriptors.  Each has the following format:

There are many other ways to print the solution graph.  Just ask if there is one you want that is not listed here.