545 lines
22 KiB
Plaintext
545 lines
22 KiB
Plaintext
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
Research Report AI-1989-02
|
|||
|
Artificial Intelligence Programs
|
|||
|
The University of Georgia
|
|||
|
Athens, Georgia 30602
|
|||
|
|
|||
|
Available by ftp from
|
|||
|
aisun1.ai.uga.edu
|
|||
|
(128.192.12.9)
|
|||
|
|
|||
|
Series editor:
|
|||
|
Michael Covington
|
|||
|
mcovingt@aisun1.ai.uga.edu
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
A Numerical Equation Solver in Prolog
|
|||
|
|
|||
|
Michael A. Covington
|
|||
|
|
|||
|
Artificial Intelligence Programs
|
|||
|
The University of Georgia
|
|||
|
Athens, Georgia 30602
|
|||
|
|
|||
|
March 1989
|
|||
|
|
|||
|
|
|||
|
Abstract: The Prolog inference engine can be extended
|
|||
|
to solve for unknowns in arithmetic equations such as
|
|||
|
X-1=1/X or X=cos(X), whether or not the equations have
|
|||
|
analytic solutions. This is done by standard numerical
|
|||
|
methods, but two features of Prolog make the
|
|||
|
implementation easy: the ability to treat expressions
|
|||
|
as data and the ability of the program to extend
|
|||
|
itself at run time.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
1. The problem
|
|||
|
|
|||
|
The Prolog inference engine can solve for any unknown in
|
|||
|
symbolic queries, but not in arithmetic queries. For example,
|
|||
|
given the fact
|
|||
|
|
|||
|
father(michael,sharon).
|
|||
|
|
|||
|
one can ask
|
|||
|
|
|||
|
?- father(michael,X).
|
|||
|
|
|||
|
and get the answer sharon, or ask
|
|||
|
|
|||
|
?- father(X,sharon).
|
|||
|
|
|||
|
and get the answer michael. This interchangeability of unknowns
|
|||
|
extends to complex symbolic manipulations (e.g., append can be
|
|||
|
used to split a list as well as to concatenate lists), but not to
|
|||
|
arithmetic.
|
|||
|
|
|||
|
Prolog handles arithmetic the way Fortran did thirty years
|
|||
|
ago: the unknown can only be a single variable on the left of the
|
|||
|
operator is, and everything on the right must be known. Thus
|
|||
|
|
|||
|
?- X is 2 + 2.
|
|||
|
|
|||
|
gets the answer 4, but
|
|||
|
|
|||
|
?- X is 1 + 1 / X.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
2
|
|||
|
|
|||
|
fails or raises an error condition.
|
|||
|
|
|||
|
The excuse for this restriction is that Prolog cannot search
|
|||
|
the set of real numbers the way it searches the symbols in a
|
|||
|
knowledge base. As far as exhaustive search goes, this is true.
|
|||
|
However, mathematicians have been using heuristic searches to
|
|||
|
solve equations since the days of Isaac Newton. The procedures
|
|||
|
given here implement one such method, making it possible to have
|
|||
|
dialogues with the computer such as:
|
|||
|
|
|||
|
?- solve( X = 1 + 1 / X ).
|
|||
|
X = 1.618034
|
|||
|
|
|||
|
?- solve( X = cos(X) ).
|
|||
|
X = 0.739085
|
|||
|
|
|||
|
and so on.
|
|||
|
|
|||
|
|
|||
|
2. The solution
|
|||
|
|
|||
|
Prolog is an ideal language for solving equations for two
|
|||
|
reasons: equations can be treated as data, and the program can
|
|||
|
modify itself. A procedure can accept expressions as parameters,
|
|||
|
then evaluate them or even create procedures to evaluate them. In
|
|||
|
Pascal or C, by contrast, there is no simple way to introduce a
|
|||
|
wholly new equation into the program at run time.
|
|||
|
|
|||
|
The program given here solves equations by the secant
|
|||
|
method, which is one of the simplest numerical methods, though not
|
|||
|
the most robust. A different method can easily be substituted once
|
|||
|
the framework of the program is in place. Standard (Edinburgh-
|
|||
|
compatible) Prolog is required; Turbo Prolog programs cannot
|
|||
|
modify themselves in the necessary way.
|
|||
|
|
|||
|
To solve the equation
|
|||
|
|
|||
|
Left = Right
|
|||
|
|
|||
|
the secant method uses the function
|
|||
|
|
|||
|
Dif(x) = Left - Right
|
|||
|
|
|||
|
where Left and Right are expressions that contain x. The problem
|
|||
|
is then to search for an x such that Dif(x) = 0.
|
|||
|
|
|||
|
The search is begun by taking two guesses at x and comparing
|
|||
|
the values of Dif(x) for each. One of them will normally be closer
|
|||
|
to zero than the other. From this information the computer can
|
|||
|
tell whether to move toward higher or lower guesses. In fact, by
|
|||
|
assuming that Dif(x) increases or decreases linearly with x, the
|
|||
|
computer can estimate how far to move. (This is why it's called
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
3
|
|||
|
|
|||
|
the secant method -- given two guesses, the third guess is formed
|
|||
|
by extending a secant line on the graph of the function.)
|
|||
|
|
|||
|
Success is not guaranteed -- the two Dif values could be
|
|||
|
equal, or the estimate of how far to move could be misleading --
|
|||
|
but the procedure usually converges on a solution in just a few
|
|||
|
iterations. Listing 1 shows the algorithm in pseudocode form.
|
|||
|
|
|||
|
|
|||
|
3. Finding the unknown
|
|||
|
|
|||
|
Expressing this in Prolog boils down to two things: setting
|
|||
|
up the problem, and then performing the computation. The setting
|
|||
|
up is done by solve, which calls free_in, define_dif, and
|
|||
|
solve_for (Listing 2).
|
|||
|
|
|||
|
The first step is to identify the unknown -- that is, to
|
|||
|
pick out the free variable in the equation to be solved. This is
|
|||
|
done by procedure free_in, which finds the free (uninstantiated)
|
|||
|
variables in any Prolog term. This is more general than what we
|
|||
|
need, but it's always useful to build a general-purpose tool.
|
|||
|
|
|||
|
If the term contains a free variable, there are two
|
|||
|
possibilities: either the term is the variable, in which case the
|
|||
|
search is over, or else the term has a variable somewhere in its
|
|||
|
argument structure. free_in has a clause for each of these cases.
|
|||
|
|
|||
|
The second case requires that the term be decomposed into a
|
|||
|
list. The built-in predicate "univ" (=..) does this. For example,
|
|||
|
|
|||
|
a(b,c(d),e) =.. [a,b,c(d),e].
|
|||
|
|
|||
|
2+3+X =.. ['+',2,3+X]
|
|||
|
|
|||
|
Even lists can be split this way, because any list is really a
|
|||
|
two-argument structure with the dot ('.') as its principal
|
|||
|
functor. That is, the list [a,b,c] is really a.(b.(c.[])), though
|
|||
|
not all Prologs allow you to write it that way. So,
|
|||
|
|
|||
|
[a,b,c] =.. ['.',a,[b,c]].
|
|||
|
|
|||
|
Thus a list is not a special case; it can be treated just like any
|
|||
|
other complex term.
|
|||
|
|
|||
|
In free_in, the statement
|
|||
|
|
|||
|
Term =.. [_,Arg|Args].
|
|||
|
|
|||
|
discards the functor, which can't be a variable anyway, and
|
|||
|
obtains two things: the first argument, Arg, and the list of
|
|||
|
subsequent arguments, Args. It is then straightforward to search
|
|||
|
for variables in both Arg and Args. Further, because Arg can be a
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
4
|
|||
|
|
|||
|
single variable, the first clause has a chance to terminate the
|
|||
|
recursion; and if it isn't, whatever is the first element of Args
|
|||
|
on this pass will be Arg on the next recursive pass and will get
|
|||
|
examined then.
|
|||
|
|
|||
|
There is, however, a special case to rule out. The term [[]]
|
|||
|
decomposes into ['.',[],[]], givng Arg = [] and Args = [[]], which
|
|||
|
would lead to an endless loop. For this reason, free_in explicitly
|
|||
|
tests for this term and rejects it.
|
|||
|
|
|||
|
|
|||
|
4. Defining a procedure
|
|||
|
|
|||
|
The next step is to define a procedure to compute the Dif
|
|||
|
function. Recall that the argument of solve is an equation in the
|
|||
|
form Left=Right. Clearly, the Dif function is obtained by
|
|||
|
evaluating Left-Right. But how is this done?
|
|||
|
|
|||
|
There are two possibilities. One possibility would be to
|
|||
|
pass along the expression Left-Right and evaluate it whenever
|
|||
|
needed. This is easily done, because
|
|||
|
|
|||
|
X is Y.
|
|||
|
|
|||
|
will evaluate whatever expression Y is instantiated to.
|
|||
|
|
|||
|
But the other, faster, possibility is to define a procedure
|
|||
|
to do the evaluating. That's what define_dif does; it creates a
|
|||
|
procedure such as
|
|||
|
|
|||
|
dif(X,Dif) :- Dif is X - cos(X).
|
|||
|
|
|||
|
using whatever expressions the user originally supplied. The
|
|||
|
result is a procedure called dif that accepts a value of X and
|
|||
|
returns the corresponding value of the Dif function. In ALS
|
|||
|
Prolog, this dif procedure is compiled into threaded code when
|
|||
|
assert places it into the program; it runs just as fast as if it
|
|||
|
had been supplied by the original programmer. Other Prologs run it
|
|||
|
interpretively.
|
|||
|
|
|||
|
What connects the variable X to the expression Left-Right in
|
|||
|
which it is supposed to occur? This question addresses the heart
|
|||
|
of Prolog's variable scoping system. It isn't enough simply that
|
|||
|
it is called X; like-named variables in Prolog are not the same
|
|||
|
unless they occur in the same rule, fact, or goal.
|
|||
|
|
|||
|
That's why so many goals in this program have both X and
|
|||
|
Left=Right (or Left-Right) as arguments. Initially, free_in takes
|
|||
|
Left and Right and finds a variable in them. This variable may
|
|||
|
have any name, but it is unified with the variable X in solve.
|
|||
|
This same X is then passed, along with Left and Right, to
|
|||
|
define_dif, which uses it in creating the dif procedure.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
5
|
|||
|
|
|||
|
Thereafter, the X in dif is guaranteed to be a variable that
|
|||
|
occurs in Left-Right, also in dif, regardless of what the user
|
|||
|
originally called it.
|
|||
|
|
|||
|
|
|||
|
5. Solving the equation
|
|||
|
|
|||
|
The last step is to implement the secant method (Listing 3).
|
|||
|
The pseudocode in Listing 1 undergoes several changes when
|
|||
|
translated into Prolog.
|
|||
|
|
|||
|
First, Prolog has no loop constructs, so recursion is used
|
|||
|
instead. The loop is replaced by the procedure solve_aux, which
|
|||
|
calls itself. Because the recursive call is the last step of a
|
|||
|
procedure with no untried alternatives, the compiler converts it
|
|||
|
back into a loop in machine language, but conceptually, the
|
|||
|
programmer thinks in terms of recursion.
|
|||
|
|
|||
|
Second, in Prolog there is no way to change the value of an
|
|||
|
instantiated variable. This means, for example, that there is no
|
|||
|
Prolog counterpart of
|
|||
|
|
|||
|
Guess1 := Guess2
|
|||
|
|
|||
|
when Guess1 already has a value. Instead, the proper Prolog
|
|||
|
technique is to pass a new value in the same argument position on
|
|||
|
the next recursive call. Thus the procedure that begins with
|
|||
|
|
|||
|
solve_aux(...Guess1,Dif1,Guess2...) :- ...
|
|||
|
|
|||
|
ends with the recursive call
|
|||
|
|
|||
|
... solve_aux(...Guess2,Dif2,Guess3...).
|
|||
|
|
|||
|
Third, there are minor rearrangements to avoid computing
|
|||
|
Dif(X) more than once with the same value of X. These include the
|
|||
|
variable Dif2 and the passing of Dif1 as an argument from the
|
|||
|
previous recursive pass.
|
|||
|
|
|||
|
|
|||
|
6. Limits and possibilities
|
|||
|
|
|||
|
This program is intended as a demonstration of the
|
|||
|
integration of numerical methods into Prolog, not as a
|
|||
|
demonstration of numerical methods per se.
|
|||
|
|
|||
|
The secant method is simple, but far from perfect. It has
|
|||
|
trouble with some equations. For example, if two successive Dif
|
|||
|
values happen to be exactly the same distance from zero, then
|
|||
|
solve_aux will try to divide by zero. This simply fails in ALS
|
|||
|
Prolog but may cause an error message in other Prologs. This
|
|||
|
problem shows up with the equation
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
6
|
|||
|
|
|||
|
X^2 - 3*X = 0
|
|||
|
|
|||
|
which has Dif=-2 for both of the first two guesses (1 and 2). With
|
|||
|
a case very close to this, such as X^2 - 3.01*X = 0, we find that
|
|||
|
although the method should work in principle, in practice the next
|
|||
|
guess is a long way from the correct solution, and the guesses run
|
|||
|
wildly out of the range of representable numbers. And with some
|
|||
|
equations, the guesses will bounce back and forth between two
|
|||
|
values, not getting better with successive iterations [1].
|
|||
|
|
|||
|
More robust numerical methods can easily be substituted into
|
|||
|
the same program framework. The ability to solve for more than one
|
|||
|
unknown is desirable; this could be treated as a multi-variable
|
|||
|
minimization problem where the goal is to minimize abs(Dif(X))
|
|||
|
[2]. It is possible to solve systems of nonlinear equations by
|
|||
|
reducing them to systems of linear equations, which can then be
|
|||
|
solved by conventional methods.
|
|||
|
|
|||
|
The program was written in ALS Prolog and has been tested in
|
|||
|
Quintus Prolog. However, other Prologs may require minor
|
|||
|
modifications. For example, Arity Prolog 4.0 requires spaces
|
|||
|
before certain opening parentheses (e.g., 2 + (3+4) + 5 rather
|
|||
|
than 2+(3+4)+5). And it is a general limitation of real-number
|
|||
|
arithmetic that a negative number cannot be raised to a non-
|
|||
|
integer power (i.e., 4^2.5 is all right but (-4)^2.5 is not). Some
|
|||
|
Prologs assume all exponents are non-integer.
|
|||
|
|
|||
|
|
|||
|
References
|
|||
|
|
|||
|
[1] Hamming, Richard W., Introduction to Applied Numerical
|
|||
|
Analysis (New York: McGraw-Hill, 1971), especially pp. 33-55.
|
|||
|
|
|||
|
[2] Press, William H.; Flannery, Brian P.; Teukolsky, Saul A.; and
|
|||
|
Vetterling, William T., Numerical Recipes: The Art of Scientific
|
|||
|
Computing (Cambridge: Cambridge University Press, 1986),
|
|||
|
especially pp. 240-334.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
7
|
|||
|
|
|||
|
Listing 1. The secant method algorithm in pseudocode form.
|
|||
|
|
|||
|
|
|||
|
To solve Left = Right:
|
|||
|
|
|||
|
function Dif(X) = Left - Right
|
|||
|
where X occurs in Left and/or Right;
|
|||
|
|
|||
|
procedure Solve;
|
|||
|
begin
|
|||
|
Guess1 := 1;
|
|||
|
Guess2 := 2;
|
|||
|
repeat
|
|||
|
Slope := (Dif(Guess2)-Dif(Guess1))/(Guess2-Guess1);
|
|||
|
Guess1 := Guess2;
|
|||
|
Guess2 := Guess2 - (Dif(Guess2)/Slope)
|
|||
|
until Guess2 is sufficiently close to Guess1;
|
|||
|
result is Guess2
|
|||
|
end.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
8
|
|||
|
|
|||
|
Listing 2. Procedures to set up the problem.
|
|||
|
|
|||
|
|
|||
|
% solve(Left=Right)
|
|||
|
%
|
|||
|
% On entry, Left=Right is an arithmetic
|
|||
|
% equation containing an uninstantiated
|
|||
|
% variable.
|
|||
|
%
|
|||
|
% On exit, that variable is instantiated
|
|||
|
% to an approximate numeric solution.
|
|||
|
%
|
|||
|
% The syntax of Left and Right is the same
|
|||
|
% as for expressions for the 'is' predicate.
|
|||
|
|
|||
|
solve(Left=Right) :-
|
|||
|
free_in(Left=Right,X),
|
|||
|
!, /* accept only one solution of free_in */
|
|||
|
define_dif(X,Left=Right),
|
|||
|
solve_for(X).
|
|||
|
|
|||
|
|
|||
|
% free_in(Term,Variable)
|
|||
|
%
|
|||
|
% Variable occurs in Term and is uninstantiated.
|
|||
|
|
|||
|
free_in(X,X) :- % An atomic term
|
|||
|
var(X).
|
|||
|
|
|||
|
free_in(Term,X) :- % A complex term
|
|||
|
Term \== [[]],
|
|||
|
Term =.. [_,Arg|Args],
|
|||
|
(free_in(Arg,X) ; free_in(Args,X)).
|
|||
|
|
|||
|
|
|||
|
% define_dif(X,Left=Right)
|
|||
|
%
|
|||
|
% Defines a predicate to compute Left-Right
|
|||
|
% for the specified equation, given X.
|
|||
|
|
|||
|
define_dif(X,Left=Right) :-
|
|||
|
abolish(dif,2),
|
|||
|
assert((dif(X,Dif) :- Dif is Left-Right)).
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
9
|
|||
|
|
|||
|
Listing 3. Procedures to implement the secant method.
|
|||
|
|
|||
|
|
|||
|
% solve_for(Variable)
|
|||
|
%
|
|||
|
% Sets up arguments and calls solve_aux (below).
|
|||
|
|
|||
|
solve_for(Variable) :-
|
|||
|
dif(1,Dif1),
|
|||
|
solve_aux(Variable,1,Dif1,2,1).
|
|||
|
|
|||
|
|
|||
|
% solve_aux(Variable,Guess1,Dif1,Guess2,Iteration)
|
|||
|
%
|
|||
|
% Uses the secant method to find a value of
|
|||
|
% Variable that will make the 'dif' procedure
|
|||
|
% return a value very close to zero.
|
|||
|
%
|
|||
|
% Arguments are:
|
|||
|
% Variable -- Will contain result.
|
|||
|
% Guess1 -- Previous estimated value.
|
|||
|
% Dif1 -- What 'dif' gave with Guess1.
|
|||
|
% Guess2 -- A better estimate.
|
|||
|
% Iteration -- Count of tries taken.
|
|||
|
|
|||
|
|
|||
|
solve_aux(cannot_solve,_,_,_,100) :-
|
|||
|
!,
|
|||
|
write('[Gave up at 100th iteration]'),nl,
|
|||
|
fail.
|
|||
|
|
|||
|
solve_aux(Guess2,Guess1,_,Guess2,_) :-
|
|||
|
close_enough(Guess1,Guess2),
|
|||
|
!,
|
|||
|
write('[Found a satisfactory solution]'),nl.
|
|||
|
|
|||
|
solve_aux(Variable,Guess1,Dif1,Guess2,Iteration) :-
|
|||
|
write([Guess2]),nl,
|
|||
|
dif(Guess2,Dif2),
|
|||
|
Slope is (Dif2-Dif1) / (Guess2-Guess1),
|
|||
|
Guess3 is Guess2 - (Dif2/Slope),
|
|||
|
NewIteration is Iteration + 1,
|
|||
|
solve_aux(Variable,Guess2,Dif2,Guess3,NewIteration).
|
|||
|
|
|||
|
|
|||
|
% close_enough(X,Y)
|
|||
|
%
|
|||
|
% True if X and Y are the same number to
|
|||
|
% within a factor of 0.0001.
|
|||
|
%
|
|||
|
|
|||
|
close_enough(X,Y) :-
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
10
|
|||
|
|
|||
|
Quot is X / Y,
|
|||
|
Quot > 0.9999,
|
|||
|
Quot < 1.0001.
|
|||
|
|