% Copyright (c) 1990-1995 Amzi! inc.
% All rights reserved

% This is a sample of a classification expert system for identification
% of certain kinds of birds. The rules are rough excerpts from 'Birds of
% North America' by Robbins, Bruum, Zim, and Singer.

% This type of expert system can easily use Prolog's built in inferencing
% system. While trying to satisfy the goal 'bird' it tries to satisfy
% various subgoals, some of which will ask for information from the
% user.

% The information is all stored as attribute-value pairs. The attribute
% is represented as a predicate, and the value as the argument to the
% predicate. For example, the attribute-value pair 'color-brown' is
% stored 'color(brown)'.

% 'identify' is the high level goal that starts the program. The
% predicate 'known/3' is used to remember answers to questions, so it
% is cleared at the beginning of the run.

% The rules of identification are the bulk of the code. They break up
% the problem into identifying orders and families before identifying
% the actual birds.

% The end of the code lists those attribute-value pairs which need
% to be asked for, and defines the predicate 'ask' and 'menuask'
% which are used to get information from the user, and remember it.

main :- identify.

identify:-
  retractall(known(_,_,_)),         % clear stored information
  bird(X),
  write('The bird is a '),write(X),nl.
identify:-
  write('I can''t identify that bird'),nl.

order(tubenose):-
  nostrils(external_tubular),
  live(at_sea),
  bill(hooked).
order(waterfowl):-
  feet(webbed),
  bill(flat).
order(falconiforms):-
  eats(meat),
  feet(curved_talons),
  bill(sharp_hooked).
order(passerformes):-
  feet(one_long_backward_toe).

family(albatross):-
  order(tubenose),
  size(large),
  wings(long_narrow).
family(swan):-
  order(waterfowl),
  neck(long),
  color(white),
  flight(ponderous).
family(goose):-
  order(waterfowl),
  size(plump),
  flight(powerful).
family(duck):-
  order(waterfowl),
  feed(on_water_surface),
  flight(agile).
family(vulture):-
  order(falconiforms),
  feed(scavange),
  wings(broad).
family(falcon):-
  order(falconiforms),
  wings(long_pointed),
  head(large),
  tail(narrow_at_tip).
family(flycatcher):-
  order(passerformes),
  bill(flat),
  eats(flying_insects).
family(swallow):-
  order(passerformes),
  wings(long_pointed),
  tail(forked),
  bill(short).

bird(laysan_albatross):-
  family(albatross),
  color(white).
bird(black_footed_albatross):-
  family(albatross),
  color(dark).
bird(fulmar):-
  order(tubenose),
  size(medium),
  flight(flap_glide).
bird(whistling_swan):-
  family(swan),
  voice(muffled_musical_whistle).
bird(trumpeter_swan):-
  family(swan),
  voice(loud_trumpeting).
bird(canada_goose):-
  family(goose),
  season(winter),                % rules can be further broken down
  country(united_states),        % to include regions and migration
  head(black),                   % patterns
  cheek(white).
bird(canada_goose):-
  family(goose),
  season(summer),
  country(canada),
  head(black), 
  cheek(white).
bird(snow_goose):-
  family(goose),
  color(white).
bird(mallard):-
  family(duck),                  % different rules for male
  voice(quack),
  head(green).
bird(mallard):-
  family(duck),                  % and female
  voice(quack),
  color(mottled_brown).
bird(pintail):-
  family(duck),
  voice(short_whistle).
bird(turkey_vulture):-
  family(vulture),
  flight_profile(v_shaped).
bird(california_condor):-
  family(vulture),
  flight_profile(flat).
bird(sparrow_hawk):-
  family(falcon),
  eats(insects).
bird(peregrine_falcon):-
  family(falcon),
  eats(birds).
bird(great_crested_flycatcher):-
  family(flycatcher),
  tail(long_rusty).
bird(ash_throated_flycatcher):-
  family(flycatcher),
  throat(white).
bird(barn_swallow):-
  family(swallow),
  tail(forked).
bird(cliff_swallow):-
  family(swallow),
  tail(square).
bird(purple_martin):-
  family(swallow),
  color(dark).

country(united_states):- region(new_england).
country(united_states):- region(south_east).
country(united_states):- region(mid_west).
country(united_states):- region(south_west).
country(united_states):- region(north_west).
country(united_states):- region(mid_atlantic).

country(canada):- province(ontario).
country(canada):- province(quebec).
country(canada):- province(etc).

region(new_england):-
  state(X),
  member(X, [massachusetts, vermont, etc]).
region(south_east):-
  state(X),
  member(X, [florida, mississippi, etc]).

region(canada):-
  province(X),
  member(X, [ontario,quebec,etc]).

nostrils(X):- ask(nostrils,X).
live(X):- ask(live,X).
bill(X):- ask(bill,X).
size(X):- menuask(size,X,[large,plump,medium,small]).
eats(X):- ask(eats,X).
feet(X):- ask(feet,X).
wings(X):- ask(wings,X).
neck(X):- ask(neck,X).
color(X):- ask(color,X).
flight(X):- menuask(flight,X,[ponderous,powerful,agile,flap_glide,other]).
feed(X):- ask(feed,X).
head(X):- ask(head,X).
tail(X):- menuask(tail,X,[narrow_at_tip,forked,long_rusty,square,other]).
voice(X):- ask(voice,X).
season(X):- menuask(season,X,[winter,summer]).
cheek(X):- ask(cheek,X).
flight_profile(X):- menuask(flight_profile,X,[flat,v_shaped,other]).
throat(X):- ask(throat,X).
state(X):- menuask(state,X,[massachusetts,vermont,florida,mississippi,etc]).
province(X):- menuask(province,X,[ontario,quebec,etc]).

% 'ask' is responsible for getting information from the user, and remembering
% the users response. If it doesn't already know the answer to a question
% it will ask the user. It then asserts the answer. It recognizes two
% cases of knowledge: 1) the attribute-value is known to be true,
% 2) the attribute-value is known to be false.

% This means an attribute might have multiple values. A third test to
% see if the attribute has another value could be used to enforce
% single valued attributes. (This test is commented out below)

% For this system the menuask is used for attributes which are single
% valued

% 'ask' only deals with simple yes or no answers. a 'yes' is the only
% yes value. any other response is considered a 'no'.

ask(Attribute,Value):-
  known(yes,Attribute,Value),       % succeed if we know its true
  !.                                % and dont look any further
ask(Attribute,Value):-
  known(_,Attribute,Value),         % fail if we know its false
  !, fail.

ask(Attribute,_):-
  known(yes,Attribute,_),           % fail if we know its some other value.
  !, fail.                          % the cut in clause #1 ensures that if
                                    % we get here the value is wrong.
ask(A,V):-
  write(A:V),                       % if we get here, we need to ask.
  write('? (yes or no): '),
  read(Y),                          % get the answer
  asserta(known(Y,A,V)),            % remember it so we dont ask again.
  Y = yes.                          % succeed or fail based on answer.

% 'menuask' is like ask, only it gives the user a menu to to choose
% from rather than a yes on no answer. In this case there is no
% need to check for a negative since 'menuask' ensures there will
% be some positive answer.

menuask(Attribute,Value,_):-
  known(yes,Attribute,Value),       % succeed if we know
  !.
menuask(Attribute,_,_):-
  known(yes,Attribute,_),           % fail if its some other value
  !, fail.

menuask(Attribute,AskValue,Menu):-
  nl,write('What is the value for '),write(Attribute),write('?'),nl,
  display_menu(Menu),
  write('Enter the number of choice> '),
  read(Num),nl,
  pick_menu(Num,AnswerValue,Menu),
  asserta(known(yes,Attribute,AnswerValue)),
  AskValue = AnswerValue.           % succeed or fail based on answer

display_menu(Menu):-
  disp_menu(1,Menu), !.             % make sure we fail on backtracking

disp_menu(_,[]).
disp_menu(N,[Item | Rest]):-        % recursively write the head of
  write(N),write(' : '),write(Item),nl, % the list and disp_menu the tail
  NN is N + 1,
  disp_menu(NN,Rest).

pick_menu(N,Val,Menu):-
  integer(N),                       % make sure they gave a number
  pic_menu(1,N,Val,Menu), !.        % start at one
  pick_menu(Val,Val,_).             % if they didn't enter a number, use
                                    % what they entered as the value

pic_menu(_,_,none_of_the_above,[]). % if we've exhausted the list
pic_menu(N,N, Item, [Item|_]).      % the counter matches the number
pic_menu(Ctr,N, Val, [_|Rest]):-
  NextCtr is Ctr + 1,               % try the next one
  pic_menu(NextCtr, N, Val, Rest).
