[net.ai] Interpreting Kastner's Preference Rules in Prolog

O'KeefeHPS@sri-unix.UUCP (09/15/84)

From:  O'Keefe HPS (on ERCC DEC-10)

[Forwarded from the Prolog Digest by Laws@SRI-AI.  This is a declarative
specification of an expert-system interpreter. -- KIL]


I've always been quite impressed by the "EXPERT" stuff being
done at Rutgers, and when I read Kastner's thesis

        Kastner, J.K.
        @i"Strategies for Expert Consultation in Therapy Planning."
        Technical Report CMB-TR-135, Department of Computer Science,
        Rutgers University, October 1983.  (PhD thesis)

I decided to write an interpreter for his rules in Prolog as an
exercise.  The first version just came up with the answer, that's the
stuff that's commented out below.  The second version left behind
information for "explanations":

    chosen(Answer, Reason, WouldHaveBeenPreferred)

Answer was the answer, Reason was the text the rule writer
gave to explain his default ordering of the treatments, and
WouldHaveBeenPreferred are the treatments we'd have preferred
in this ordering if they hadn't been contraindicated

    despite(Answer, Contraindications)

means that Answer was contraindicated by each of the problems
listed, but it was still picked because the preferred choices
had worse problems.

    rejected(Treatment, Contraindications)

means that Treatment was rejected because it had the problems
listed.  Every treatment will be rejected or chosen.  Note: in
these two facts the Contraindications are those which were
checked and found to be applicable, less severe ones may not
have been checked.  (This is a feature, the whole point of the
code in fact.)

You'll have to read Kastner's thesis to see how these rules are used,
but if you're interested in Expert Systems you'll want to read it.

Why have I sent this to the [Prolog] Digest?  Two reasons.  (1) someone
may have a use for it, and if I send it to the library it'll sink without
trace.  (2) I'm quite pleased with the "no-explanations" version, but
the "explanations" version is a bit of a mess, and if anyone can find
a cleaner way of doing it I'd be very pleased to see it.  I guess I
still don't know how best to do data base hacking.

A point which may be interesting: I originally had worst/6 binding its
second argument to 'none' where there were no new contraindications.
The mess which resulted (though it worked) reminded me of a lesson I
thought I'd learned before: it is dangerous to have an answer saying
there are no answers, because that looks like an answer.  All the
problems I had with this code came from thinking procedurally.

:-  op(900, fx, 'Is ').
:-  op(899, xf, ' true').
:-  compile([
        'util:ask.pl',          % for yesno/1
        'util:projec.pl',       % for project/3
        'prefer.pl'             % which follows
    ]).

%   File   : PREFER.PL
%   Author : R.A.O'Keefe
%   Updated: 14 September 1984
%   Purpose: Interpret Kastner's "preference rules" in Prolog

:- public
        go/0,
        og/0.

:- mode
        prefer(-, +, +, +),
        pass(+, +, +, -),
        pass(+, +, +, +, +, +, -),
        worst(+, -, +, +, -, -),
        chose(+, +, +),
        forget(+, +),
        compare_lengths(+, +, -),
        evaluate(+).


prefer(Treatment, Rationale, Contraindications, Columns) :-
        pass(Columns, [], Contraindications, Treatment),
        append(Pref1, [Treatment=_|_], Columns), !,
        project(Pref1, 1, Preferred),
        assert(chosen(Treatment, Rationale, Preferred)).


pass([Tr=Tests|U], Cu, Vu, T) :-
        worst(Tests, Rest, Cu, Vu, Cb, Vb), !,
        pass(U, [Tr=Rest], Cu, Vu, Cb, Vb, T).
pass([T=_|U], C, _, T) :-
        chose(T, U, C).


pass([], [T=_], _, _, C, _, T) :- !,
        chose(T, [], C).
pass([], B, _, _, Cb, Vb, T) :-
        reverse(B, R),
        pass(R, Cb, Vb, T).
pass([Tr=Tests|U], B, Cu, Vu, Cb, Vb, T) :-
        worst(Tests, Rest, Cu, Vu, Ct, Vt),
        compare_lengths(Vt, Vb, R),
        (   R = (<), C1 = Ct, V1 = Vt, B1 = [Tr=Rest], forget(B, Cb)
        ;   R = (=), C1 = Cb, V1 = Vb, B1 = [Tr=Rest|B]
        ;   R = (>), C1 = Cb, V1 = Vb, B1 = B, assert(rejected(Tr,Ct))
        ),  !,          % moved down from worst/6 for "efficiency"
        pass(U, B1, Cu, Vu, C1, V1, T).
pass([T=_|_], B, _, _, C, _, T) :-
        chose(T, B, C).


worst([Test|Tests], Tests, C, [X|V], [X|C], V) :-
        evaluate(Test), !.
worst([_|Tests], Rest, Cu, [_|Vu], Ct, Vt) :-
        worst(Tests, Rest, Cu, Vu, Ct, Vt).


evaluate(fail) :- !, fail.
evaluate(Query) :-
        known(Query, Value), !,
        Value = yes.
evaluate(Query) :-
        yesno('Is ' Query ' true'),
        !,
        assert(known(Query, yes)).
evaluate(Query) :-
        assert(known(Query, no)),
        fail.


chose(Treatment, Rejected, Contraindications) :-
        assert(despite(Treatment, Contraindications)),
        forget(Rejected, Contraindications).


forget([], _).
forget([Treatment=_|Rejected], Contraindications) :-
        assert(rejected(Treatment, Contraindications)),
        forget(Rejected, Contraindications).


compare_lengths([], [], =).
compare_lengths([],  _, <).
compare_lengths( _, [], >).
compare_lengths([_|List1], [_|List2], R) :-
        compare_lengths(List1, List2, R).


/*----------------------------
%  Version that doesn't store explanation information:

prefer(Treatment, Rationale, Contraindications, Columns) :-
        pass(Columns, 0, [], Treatment).


pass([], _, [T=_], T) :- !.
pass([], _, B, T) :-
        reverse(B, R),
        pass(R, 0, [], T).
pass([Tr=Col|U], I, B, T) :-
        worst(Col, 1, W, Reduced),
        !,
        (   W > I, pass(U, W, [Tr=Reduced], T)
        ;   W < I, pass(U, I, B, T)
        ;   W = I, pass(U, I, [Tr=Reduced|B], T)
        ).
pass([T=_|_], _, _, T).         % no (more) contraindications


worst([], _, none, []).
worst([Condition|Rest], Depth, Depth, Rest) :-
        evaluate(Condition), !.
worst([_|Col], D, W, Residue) :-
        E is D+1,
        worst(Col, E, W, Residue).