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.
|
||
|
||
|