AI Expert Newsletter
AI  The art and science of making computers do interesting
things that are not in their nature.
February 2004
The February 2004 newsletter is really a companion piece for the
January 2004 newsletter. January discussed fuzzy logic and its usefulness
for control systems, and included discussions of some simple examples.
This month's newsletter has the code used for the experiments in
January. It is a fuzzy logic laboratory intended for modification
and experimentation.
The full code for the system is included in the newsletter, but
if you are interested in getting the files directly, send me an
email.
As always, any and all feedback is appreciated,
Dennis
Code Corner
The January issue talked about fuzzy logic and described some experiments
with fuzzy logic and showers. There are a number of tools available
for playing with fuzzy logic, but, as usual, its always fun to create
your own. This month we'll describe the fuzzy logic system built
for conducting experiments with control systems. The system includes
the fuzzy reasoning engine and simulation capability for testing.
The knowledge representation language used to build a fuzzy control
system is framebased, as our other rulebased systems have been
(see recent newsletters for other examples). There are frames for
defining fuzzy sets, fuzzy rules, normal rules, and forward chaining
rules for controlling the system. We'll visit each them as we walk
through the coding of the single knob shower example, described
in the Jan 2004 issue.
Knowledge Representation  Single Knob
Shower
Fuzzy sets are defined over a crisp domain variable. The supported
set types are defined by straight lines, which work well. The options
are:
 descending line, defined by the domain variable where the fuzzy
membership is 1.0 and 0.0,
 ascending line, defined by the domain variable at 0.0 and 1.0,
 triangle, defined by the domain variable at 0.0, 1.0, and 0.0,
and
 trapezoid, defined by the domain variable at 0.0, 1.0, 1.0 and
0.0.
Other fancier sets with logarithmic curves are possible, and could
be added to the system as enhancements.
These are the input and output sets described in the January 2004
issue:
And this is how they are encoded:
fuzzy_set(water_temperature, [
variable(cold) :: descending_line( 80.0, 100.0 ),
variable(just_right) :: triangle( 90.0, 100.0, 110.0 ),
variable(hot) :: ascending_line( 100.0, 120.0 )
]).
fuzzy_set(rotate, [
variable(left) :: descending_line( 30.0, 0.0 ),
variable(none) :: triangle( 15.0, 0.0, 15.0 ),
variable(right) :: ascending_line( 0.0, 30.0 )
]).
Next are the fuzzy rules that relate the input fuzzy sets to the
output fuzzy sets. They are read as: if water_temperature is
hot then rotate right, etc. Note that the fuzzy rules use 'fzis'
instead of is. This allows the system to support other rules as
well as fuzzy ones, and also lets the fuzzy part of the system be
more easily integrated into some other larger system.
The rules support: fzis (is), fzand (and), fzor (or), fznot (not)
and the hedges very (square), slightly (sqrt) and hnot (not). There
can be multiple rules for a single variable, in which case the results
of the two rules are fzor'd together. The two knob example has more
complex rules.
fuzzy_rules(rotate, [
do(right) :: water_temperature fzis hot,
do(none) :: water_temperature fzis just_right,
do(left) :: water_temperature fzis cold
]).
Normal, nonfuzzy, rules are needed for other aspects of the simulation.
They are supported with a slot indicating the conditions under which
they apply and the value of the variable. Sometimes the value will
be computed from a formula, which is really a list of Prolog statements.
Notice in this case the call to get_av/2. This is the key
predicate of the system that finds a value for an attribute, triggering
the reasoning engine as necessary. It would be nicer if this didn't
appear in the external programming interface, as it does here, and
the user could simply include a variable, say dial_angle, without
having to call get_av(dial_angle, ANGLE). But that's an enhancement
for a later version.
These following two rules are not really part of the control system,
but are part of the simulation used to test the control system.
They calculate the value of the water temperature based on the position
of control. The calculation is different, depending on whether the
toilet is flushing or not. A flush has the effect of making it appear
as if the control is moved 20 degrees hotter.
rule(water_temperature, [
conditions :: flush = true,
formula(TEMP) :: [
get_av(dial_angle, ANGLE),
OFFSET is ANGLE 20  (90),
TEMP is 140 * (180OFFSET) / 180 + 50 * OFFSET / 180 ]
]).
rule(water_temperature, [
conditions :: not flush = true,
formula(TEMP) :: [
get_av(dial_angle, ANGLE),
OFFSET is ANGLE  (90),
TEMP is 140 * (180OFFSET) / 180 + 50 * OFFSET / 180 ]
]).
Notice that the above math is only necessary for the simulation
part of the system. If this were a real controller for a shower,
the designer of the controller wouldn't have to know the equations
that describe the water temperature, the real shower would provide
that data.
Simpler, nonformula rules are supported too. In that case the
rule just has a value. The following rule uses a variable called
time_ticks which is just a counter the reasoning engine maintains
for loops through the simulation. The rules indicate when the toilet
is flushing. The second rule just has 'true' for a condition because
it is using the fact that the rules for a given variable, in this
case 'flush', are tried in the order presented.
rule(flush, [
conditions :: time_ticks > 10 and time_ticks < 20,
value :: true
]).
rule(flush, [
conditions :: true,
value :: false
]).
Control frames are used to control the simulation. Each has a condition
under which it is applied, and a set of actions to take when it
is applied. Notice that the conditions can refer to fuzzy variables,
which need to be converted to a crisp boolean. A value of 0.8 is
used as the threshold.
The first rule below fires when the water_temperature is the just_right
fuzzy set is 0.8 or greater. It.simply updates the state of flush
and water_temperature, and reports that all is OK. The second rule
does the work when needed, first calling for an update of 'rotate'.
This triggers the fuzzy rules for rotate which are based on the
fuzzy sets over temperature. Having learned the rotation, the dial_angle
is update by the degree of rotation, and the simulation formula
for water_temperature is recalculated for the next pass through
the loop.
The main control loop finds a control rule that applies, uses it,
and then loops again. It is the application's responsibility to
code actions, such as these, that reset the critical variables for
each iteration. In this example, the flush and water_temperature
are updated each cycle, and the degree of rotation is updated when
changed.
control(ok, [
conditions :: water_temperature fzis just_right,
actions :: [
update(flush),
update(water_temperature),
report([`ok `, flush, ` `, dial_angle, ` `, water_temperature, ` `]) ]
]).
control(not_ok, [
conditions :: not water_temperature fzis just_right,
actions :: [
update(rotate, Rotation),
get_av(dial_angle, OldA),
NewAngle is OldA + Rotation,
set(dial_angle, NewAngle),
update(flush),
update(water_temperature),
report([flush, ` `, dial_angle, ` `, water_temperature, ` `]) ]
]).
Finally, the beginning. The start frame is used to initialize the
system, and different starts can be used for different test scenarios.
In is mandatory to set the test_duration, otherwise the simulation
might go forever, or not at all. In this case, an initial dial_angle
is chosen, on the chilly side, and the water_temperature is udpated
based on it. This is then used as a start for the control loop.
start(one, [
actions :: [
set(dial_angle, 50),
set(test_duration, 30),
update(water_temperature),
report([`Start test one`, ` `, dial_angle, ` `, water_temperature, ` `]) ]
]).
Running the System
To run the system, consult, compile, or otherwise run the main/0
predicate of the fuzzy.pro reasoning engine. It will prompt for
the name of a .fuz file and the name of a start scenario to use.
On my system the single shower system above is called shower_1.fuz,
so running it looks like this:
? main.
Which .fuz file to run? shower_1
Which start to use? one
Start test one dial_angle = 50 water_temperature = 70
time = 0 flush = false dial_angle = 30 water_temperature = 80
time = 1 flush = false dial_angle = 10 water_temperature = 90
time = 2 flush = false dial_angle = 8.33333 water_temperature = 99.1667
time = 3 ok flush = false dial_angle = 8.33333 water_temperature = 99.1667
time = 4 ok flush = false dial_angle = 8.33333 water_temperature = 99.1667
...
time = 11 ok flush = true dial_angle = 8.33333 water_temperature = 109.167
time = 12 flush = true dial_angle = 6.42704 water_temperature = 101.786
time = 13 ok flush = true dial_angle = 6.42704 water_temperature = 101.786
time = 14 ok flush = true dial_angle = 6.42704 water_temperature = 101.786
...
time = 20 ok flush = false dial_angle = 6.42704 water_temperature = 91.7865
time = 21 flush = false dial_angle = 5.46122 water_temperature = 97.7306
time = 22 flush = false dial_angle = 8.37675 water_temperature = 99.1884
time = 23 ok flush = false dial_angle = 8.37675 water_temperature = 99.1884
time = 24 ok flush = false dial_angle = 8.37675 water_temperature = 99.1884
...
done
yes
?
Reasoning Engine
The
reasoning engine was harder to implement than I had hoped, but that
was primarily because I found it difficult to find clear formulas
for the centroid defuzzification algorithm. Defuzzification is
the process of taking the fuzzy set values on a domain and converting
them to a crisp value.
The best I could find was the definition that the centroid about
the xaxis, C_{x}, of a function f(x) was defined as the
moment / area.
C_{x} = ò x ×
f(x) dx / ò f(x) dx
Its been a long time since I've had to do calculus and some of
the links below are to the math sites I usedto refresh my memory.
My desk was filled with bits of paper like that on the right, as
I tried to remember what the integral of x squared was and how to
calculate slope and intercept from points on a straight line.
I worked out the centroids for some simple shapes, and they have
relatively simple formulas. For example, the centroid for a rectangle
is simply the average of the two x coordinates, and for a right
triangle, its 2/3 of the way towards the tall side.
But I was not able to find a simplification for the case of the
centroid for the shape beneath an arbitrary line segment, and it
seemed that it would be of the most general use. Here's what I came
up with for how to caculate the centroid and area under a line segment
(X1,Y1) (X2,Y2), where M is slope, B is Y intercept, Mx is the moment
about the X axis, Cx is the centroid, and A is the area:
M is (Y2Y1)/(X2X1),
B is Y2  M * X2,
A is M*X2*X2/2 + B*X2  M*X1*X1/2  B*X1,
Mx is M*X2*X2*X2/3 + B*X2*X2/2  M*X1*X1*X1/3  B*X1*X1/2,
Cx is Mx/A.
Having explained that, the rest of the code is not dissimilar from
other rulebased systems described in this newsletter. Here is the
rest of the code:
The operator definitions support the fuzzy operators.
: op(950, xfx, ::). % for slots
: op(920, xfy, or). % conventional rules
: op(910, xfy, and). % conventional rules
: op(920, xfy, fzor). % fuzzy rules
: op(910, xfy, fzand). % fuzzy rules
: op(900, fy, fznot). % fuzzy rules
: op(700, xfx, fzis). % fuzzy rules
: op(600, fy, very). % fuzzy hedge
: op(600, fy, slightly). % fuzzy hedge
: op(600, fy, hnot). % fuzzy hedge
The main entry point loads a logic base, finds the start, initializes
the system and starts the control loop. An external host language
using the system could emulate this code, allowing the fuzzy logic
engine to be embedded .
main :
prompt(`Which .fuz file to run? `, File),
strcat(File, `.fuz`, FuzFile),
reconsult(FuzFile),
prompt(`Which start to use? `, Start),
initialize(Start),
known(test_duration, Duration),
control_loop(Duration),
write(done),
nl.
These are the I/O statements used by the engine. They allow for
the possibility that extended predicates outside of the Prolog environment
have been defined to handle I/O. This would be the case for an embedded
application.
prompt(Q,A) :
ext_prompt(Q,A),
!.
prompt(Question, Answer) :
write(Question),
read_string(Answer).
output(X) :
ext_output(X),
!.
output(nl) :
!, nl.
output(X) :
write(X).
Initialize the timer, take the actions required by the start.
initialize(TEST) :
abolish( known/2 ),
assert( known(time_ticks,0) ),
start(TEST, Slots),
get_slot(actions::Actions, Slots),
take(Actions).
The control loop is a forward chaining loop. It looks for a rule
that can fire and fires it. The rule actions will cause the state
of the system to change so the next time through the loop maybe
a different rule will fire.
control_loop(Duration) :
get_av(time_ticks,Duration),
!.
control_loop(Duration) :
get_av(time_ticks,Ticks),
write(time=Ticks), tab(2),
control(_, Slots),
get_slot(conditions::Conditions, Slots),
test(Conditions),
get_slot(actions::Actions, Slots),
take(Actions),
Ticks2 is Ticks+1,
set(time_ticks,Ticks2),
!,
control_loop(Duration).
The basic utility used to retrieve slots from frames.This allows
two forms of query.
get_slot(S, [S_]).
get_slot(S, [_Slots]) :
get_slot(S, Slots).
get_slot(Slot, Val, [Slot :: Val  _]).
get_slot(Slot, Val, [_SVs]) :
get_slot(Slot, Val, SVs).
The nonfuzzy rules and the control rules have conditions under
which they apply. These are the tests that are supported as conditions.
The conditions can be in a list or boolean expression. Note that
the last rule is a catchall, and assumes that if none of the other
test patterns applied, then the test conditions are probably fuzzy.
In that case the fuzzy rule is applied and the fuzzy value that
results is compared to 0.8, the criteria for absolute truth.
test([]) :
!.
test([ConditionConditions]) :
test(Condition),
!,
test(Conditions).
test( C1 and C2 ) :
test(C1),
test(C2).
test( C1 or C2 ) :
(test(C1); test(C2)).
test( Attr = Val ) :
get_av(Attr, Val).
test( not C ) :
not(test(C)).
test( Attr > Val ) :
get_av(Attr,X),
X > Val.
test( Attr >= Val ) :
get_av(Attr,X),
X >= Val.
test( Attr < Val ) :
get_av(Attr,X),
X < Val.
test( Attr =< Val ) :
get_av(Attr,X),
X =< Val.
test(true).
test( Fuzzy ) :
apply_fuzzy_rule(Fuzzy, Val),
!,
Val >= 0.8.
These are the actions that can be taken in a control rule. Note
that there is a catchall clause that allows any Prolog goal to
be called as well. Given this, its not really necessary to have
the other clauses, except that at some point it would be nice if
there was a specific list of allowable actions, and for that the
individual take/1 clauses will be perfect.
take([]) :
!.
take([ActionActions]) :
take(Action),
!,
take(Actions).
take( set(Attr,Val) ) :
!, set(Attr,Val).
take( update(Attr,Val) ) :
!, update(Attr,Val).
take( update(Attr) ) :
!, take( update(Attr,_) ).
take( report(Items) ) :
!, report(Items).
take( X ) :
call(X),
!.
take( X ) :
output(nl),
output( error:X ),
output(nl),
fail.
clear(Attr) :
retract( known(Attr,_) ),
!.
clear(_).
set(Attr,Val) :
clear(Attr),
assert( known(Attr,Val) ).
update(Attr,Val) :
clear(Attr),
get_av(Attr,Val).
report([]) :
output(nl).
report([TextItems]) :
string(Text),
output(Text),
!,
report(Items).
report([nlItems]) :
output(nl),
!,
report(Items).
report([AttrItems]) :
get_av(Attr,Val),
output(Attr = Val),
!,
report(Items).
report([XItems]) :
output(X),
!,
report(Items).
get_av/2 (get attribute value) is the heart of the reasoning engine.
It looks for the value of an attribute. If the value isn't already
known, then it looks for rules that can be used to compute the value.
Those rules might need other attributes, and thus call get_av/2
as well in a recursive reasoning loop.
get_av(Attr, Val) :
known(Attr, X),
!,
Val = X.
get_av(Attr, Val) :
rule(Attr, Slots),
get_slot(conditions::Conditions, Slots),
test(Conditions),
get_value(X, Slots),
assert( known(Attr,X) ),
!,
Val = X.
get_av(Attr, Val) :
F =.. [Attr, X],
formula(F, EQ),
evaluate(EQ),
assert(known(Attr,X)),
!,
Val = X.
get_av(Attr, Val) :
fuzzy_rules(Attr, Rules),
apply_fuzzy_rules(Rules, CrispVal),
assert(known(Attr,CrispVal)),
!,
Val = CrispVal.
Values in slots might be simple values, or they might require
the evaluation of a formula.
get_value(X, Slots) :
get_slot(value::X, Slots),
!.
get_value(X, Slots) :
get_slot(formula(X)::EQ, Slots),
evaluate(EQ),
!.
A formula might involve multiple steps. Prolog unification takes
care of the variable bindings from step to step.
evaluate([]).
evaluate([EQEQs]) :
call(EQ),
!,
evaluate(EQs).
apply_fuzzy_rules/2 is the heart of the fuzzy part of the system.
It takes the rules used to determine an output value, combines all
the appropriate fuzzy sets and then defuzzifies the resulting output
collection of fuzzy set values. It calls apply_fuzzy_rules/3 with
the additional argument used to build the output list.
apply_fuzzy_rules(Rules, CrispVal) :
apply_fuzzy_rules(Rules, [], FuzzyVals),
defuzzify(Attr, FuzzyVals, CrispVal).
There might be multiple rules for a given fuzzy set. If so the
results of those rules are fuzzy or'd together. The result of the
predicate is a list of unique fuzzy_set fuzzy_value pairs. For example,
[left:0.2, none:0.6].
apply_fuzzy_rules([], SoFar, Results) :
sort(SoFar, Sorted),
resolve_ors(Sorted, Results),
!.
apply_fuzzy_rules([do(Var)::Rule  Attrs], SoFar, Results) :
apply_fuzzy_rule(Rule, FuzzyVal),
!,
apply_fuzzy_rules(Attrs, [Var:FuzzyVal  SoFar], Results).
resolve_ors([], []).
resolve_ors([A:V1, A:V2AVs], Out) :
max(V1,V2,V),
!,
resolve_ors([A:VAVs], Out).
resolve_ors([AVAVs], [AVOut]) :
resolve_ors(AVs, Out).
max(X,Y,X) : X >= Y, !.
max(X,Y,Y).
min(X,Y,X) : X =< Y, !.
min(X,Y,Y).
A fuzzy rule might be a complex boolean expression. These rules
implement the rules for fuzzy and, or, and not. A phrase of a fuzzy
rule might contain a hedge, where a hedge is a modifier of a value,
such as very. The fuzzify/4 predicate used in the fourth clause
gets the fuzzy value in a set from a crisp input value.
apply_fuzzy_rule(C1 fzand C2, FuzzyVal) :
!,
apply_fuzzy_rule(C1, F1),
apply_fuzzy_rule(C2, F2),
min(F1,F2,FuzzyVal).
apply_fuzzy_rule(C1 fzor C2, FuzzyVal) :
!,
apply_fuzzy_rule(C1, F1),
apply_fuzzy_rule(C2, F2),
max(F1,F2,FuzzyVal).
apply_fuzzy_rule(fznot C, FuzzyVal) :
!,
apply_fuzzy_rule(C, F),
FuzzyVal is 1.0  F.
apply_fuzzy_rule(Attr fzis HedgedVar, HedgedFuzzyVal) :
get_av(Attr, AttrVal),
strip_hedges(HedgedVar, Var),
fuzzify(Attr, AttrVal, Var, FuzzyVal),
apply_hedges(HedgedVar, FuzzyVal, HedgedFuzzyVal).
strip_hedges(HedgeVar, Var) :
HedgeVar =.. [Hedge, HVar],
!,
strip_hedges(HVar, Var).
strip_hedges(Var, Var).
apply_hedges(very HVar, Val, HedgedVal) :
apply_hedges(HVar, Val, HVal),
!,
HedgedVal is HVal * HVal.
apply_hedges(slightly HVar, Val, HedgedVal) :
apply_hedges(HVar, Val, HVal),
!,
HedgedVal is sqrt(HVal).
apply_hedges(hnot HVar, Val, HedgedVal) :
apply_hedges(HVar, Val, HVal),
!,
HedgedVal is 1.0  HVal.
apply_hedges(_, Val, Val).
Given the attribute (Attr), the crisp value (AttrVal) of the attribute,
and the name of the fuzzy variable set (Var), compute the fuzzy
value. For example, if the fuzzy set cold was defined on temperature
as a descending line from 80 degrees to 100, and the temperature
was 90,then the fuzzy value would be 0.5.
fuzzify(Attr, AttrVal, Var, FuzzyVal) :
fuzzy_set(Attr, Vars),
get_slot(variable(Var), SetDefinition, Vars),
fuzz(SetDefinition, AttrVal, FuzzyVal).
Any number of possible shapes of fuzzy sets could be supported.
These are the ones supported now.
fuzz(descending_line(X,Y), A, 1.0) : A =< X, !.
fuzz(descending_line(X,Y), A, 0.0) : A >= Y, !.
fuzz(descending_line(X,Y), A, B) : B is (YA)/(YX), !.
fuzz(ascending_line(X,Y), A, 0.0) : A =< X, !.
fuzz(ascending_line(X,Y), A, 1.0) : A >= Y, !.
fuzz(ascending_line(X,Y), A, B) : B is (AX)/(YX), !.
fuzz(triangle(X,Y,Z), A, 0.0) : A =< X, !.
fuzz(triangle(X,Y,Z), A, 0.0) : A >= Z, !.
fuzz(triangle(X,Y,Z), A, B) : A =< Y, B is (AX)/(YX), !.
fuzz(triangle(X,Y,Z), A, B) : B is (ZA)/(ZY), !.
fuzz(trapezoid(W,X,Y,Z), A, 0.0) : A =< W, !.
fuzz(trapezoid(W,X,Y,Z), A, 0.0) : A >= Z, !.
fuzz(trapezoid(W,X,Y,Z), A, 1.0) : A >= X, A =< Y, !.
fuzz(trapezoid(W,X,Y,Z), A, B) : A =< X, B is (AW)/(XW), !.
fuzz(trapezoid(W,X,Y,Z), A, B) : B is (ZA)/(ZY), !.
The tricky part is defuzzification, or the process of taking the
fuzzy values of fuzzy variables over a particular domain and converting
them to a single real value in the domain. These are the predicates
that implement the methods described at the beginning of this section.
The two 0 arguments are accumulators for the sum of the area * centroid
and the sum of the area.
defuzzify(Domain, FuzzyVals, CrispVal) :
defuzzify(FuzzyVals, Domain, 0, 0, CrispVal).
I'm not sure this is the most efficient or elegant way to code
this. First the points that define the cropped set are determined,
and then the generalized centroid calculation that computes the
centroid and area under an arbitrary line segment is used. It has
the most complicated formula. A better approach might be to use
the fact that we know whether a segment defines a rectangle or a
right triangle and use the simpler formula for those cases. The
predicates supporting those simpler calculations are included, but
not actually used.
defuzzify([], _, SumACx, SumA, Crisp) :
Crisp is SumACx/SumA.
defuzzify([_:0.0FVs], Domain, SumACx, SumA, Crisp) :
!,
defuzzify(FVs, Domain, SumACx, SumA, Crisp).
defuzzify([FVar:FValFVs], Domain, SumACx, SumA, Crisp) :
get_points(Domain, FVar, FVal, Points),
centroid(Points, Cx, A),
SumACx2 is SumACx + A*Cx,
SumA2 is SumA + A,
!,
defuzzify(FVs, Domain, SumACx2, SumA2, Crisp).
The predicates that calculate the centroid and area do so from
the x,y points that define the line segments of the cropped set.
get_points/4 gets those points and returns them in a list, [x1,y1,
x2,y2, ...]
get_points(Domain, FVar, FVal, Points) :
fuzzy_set(Domain, FVars),
get_slot(variable(FVar), FSet, FVars),
set_points(FSet, FVal, Points).
These are the points for the supported fuzzy set shapes. The Ms
and Bs in the rules are the slope and yintercept, derived from
the x,y points. In each case, the point list represents a cropped
fuzzy set.
set_points(ascending_line(X1,X2), 1.0, [X1,0.0,X2,1.0]) :
!.
set_points(ascending_line(X1,X2), YVal, [X1,0.0,XVal,YVal,X2,YVal]) :
M is 1/(X2X1),
B is M * X1,
XVal is (YValB)/M.
set_points(descending_line(X1,X2), 1.0, [X1,1.0,X2,0.0]) :
!.
set_points(descending_line(X1,X2), YVal, [X1,YVal,XVal,YVal,X2,0.0]) :
M is 1/(X2X1),
B is 1.0  M * X1,
XVal is (YValB)/M.
set_points(triangle(X1,X2,X3), 1.0, [X1,0.0, X2,1.0, X3,0.0]) :
!.
set_points(triangle(X1,X2,X3), YVal, [X1,0.0, XV1,YVal, XV2,YVal, X3,0.0]) :
M1 is 1/(X2X1),
B1 is M1 * X1,
XV1 is (YValB1)/M1,
M2 is 1/(X3X2),
B2 is 1.0  M2 * X2,
XV2 is (YVal  B2)/M2.
set_points(trapezoid(X1,X2,X3,X4), 1.0, [X1,0.0, X2,1.0, X3,1.0, X4,0.0]) :
!.
set_points(trapezoid(X1,X2,X3,X4), YVal, [X1,0.0, XV1,YVal, XV2,YVal, X4,0.0]) :
M1 is 1/(X2X1),
B1 is M1 * X1,
XV1 is (YValB1)/M1,
M2 is 1/(X4X3),
B2 is 1.0  M2 * X3,
XV2 is (YVal  B2)/M2.
Here are the notused centroid calculations for the basic shapes.
centroid/3 returns the area as well as the centroid for use in the
normalized summing process.
centroid(ascending_line(X1,X2,H), Cx, A) :
Cx is X1 + 2*(X2X1)/3,
A is H*(X2X1)/2.
centroid(descending_line(X1,X2,H), Cx, A) :
Cx is X1 + (X2X1)/3,
A is H*(X2X1)/2.
centroid(rectangle(X1,X2,H), Cx, A) :
Cx is (X2+X1)/2,
A is H*(X2X1).
This is the messiest centroid, but the most general. It computes
the centroid and area under an arbitrary line segment. If the segment
is a line, or the edge of a right triangle, this is equivalent to
the equations above. I don't know if there's a simpler way to calculate
this.
centroid(two_points(X1,Y1,X2,Y2), Cx, A) :
M is (Y2Y1)/(X2X1),
B is Y2  M * X2,
A is M*X2*X2/2 + B*X2  M*X1*X1/2  B*X1,
Mx is M*X2*X2*X2/3 + B*X2*X2/2  M*X1*X1*X1/3  B*X1*X1/2,
Cx is Mx/A.
To find the centroid under a list of points, find the centroid
under each line segment using the formula given above and then compute
the sum of the areas * centroids / sum of the areas.
centroid(PointList, Cx, A) :
list(PointList),
centroid(PointList, 0, 0, Cx, A).
centroid([_,_], SumACx, A, Cx, A) :
Cx is SumACx / A,
!.
centroid([X1,Y1,X2,Y2XYs], SumACx, SumA, Cx, A) :
centroid(two_points(X1,Y1,X2,Y2), Cx1, A1),
SumACx2 is SumACx + A1*Cx1,
SumA2 is SumA + A1,
!,
centroid([X2,Y2XYs], SumACx2, SumA2, Cx, A).
Two Knob Shower
The two knob shower is a more complex system. There are independent
hot and cold knobs, and it is desired to keep the flow, as well
as the temperature, at an optimal level. Here is the control file
for it:
fuzzy_set(water_temperature, [
variable(cold) :: descending_line( 80.0, 100.0 ),
variable(just_right) :: triangle( 90.0, 100.0, 110.0 ),
variable(hot) :: ascending_line( 100.0, 120.0 )
]).
fuzzy_set(water_flow, [
variable(low) :: descending_line( 1.5, 2.5 ),
variable(just_right) :: triangle( 2.0, 2.5, 3.0 ),
variable(high) :: ascending_line( 2.5, 3.5 )
]).
fuzzy_set(turn_hot, [
variable(close) :: descending_line( 20.0, 0.0 ),
variable(none) :: triangle( 5.0, 0.0, 5.0 ),
variable(open) :: ascending_line( 0.0, 20.0 )
]).
fuzzy_set(turn_cold, [
variable(close) :: descending_line( 20.0, 0.0 ),
variable(none) :: triangle( 5.0, 0.0, 5.0 ),
variable(open) :: ascending_line( 0.0, 20.0 )
]).
% There are two ways to write the rules for the controls, both are
% equivalent and one is used for hot and the other for cold.
fuzzy_rules(turn_hot, [
do(close) :: water_temperature fzis hot,
do(none) :: water_temperature fzis just_right,
do(open) :: water_temperature fzis cold,
do(close) :: water_flow fzis high,
do(none) :: water_flow fzis just_right,
do(open) :: water_flow fzis low
]).
fuzzy_rules(turn_cold, [
do(open) :: water_temperature fzis hot fzor water_flow fzis low,
do(none) :: water_temperature fzis just_right fzor water_flow fzis just_right,
do(close) :: water_temperature fzis cold fzor water_flow fzis high
]).
rule(water_temperature, [
conditions :: flush = true,
formula(TEMP) :: [
get_av(hot_position, HOT),
get_av(cold_position, COLD),
COLD_ADJUSTED is COLD  20,
TEMP is (HOT*140 + COLD_ADJUSTED*50) / (HOT + COLD_ADJUSTED) ]
]).
rule(water_temperature, [
conditions :: not flush = true,
formula(TEMP) :: [
get_av(hot_position, HOT),
get_av(cold_position, COLD),
TEMP is (HOT*140 + COLD*50) / (HOT + COLD) ]
]).
rule(water_flow, [
conditions :: flush = true,
formula(FLOW) :: [
get_av(hot_position, HOT),
get_av(cold_position, COLD),
COLD_ADJUSTED is COLD  20,
FLOW is 2.0*HOT/100 + 2.0*COLD_ADJUSTED/100 ]
]).
rule(water_flow, [
conditions :: not flush = true,
formula(FLOW) :: [
get_av(hot_position, HOT),
get_av(cold_position, COLD),
FLOW is 2.0*HOT/100 + 2.0*COLD/100 ]
]).
rule(flush, [
conditions :: time_ticks > 30 and time_ticks < 40,
value :: true
]).
rule(flush, [
conditions :: true,
value :: false
]).
control(ok, [
conditions :: water_temperature fzis very just_right fzand water_flow fzis very just_right,
actions :: [
update(flush),
update(water_temperature),
update(water_flow),
report([`ok `, tab(2), flush, tab(2), hot_position, tab(2), cold_position, tab(2),
water_temperature, tab(2), water_flow]) ]
]).
control(not_ok, [
conditions :: true,
actions :: [
update(turn_hot, ChangeHot),
update(turn_cold, ChangeCold),
get_av(hot_position, OldHot),
get_av(cold_position, OldCold),
NewHot is OldHot + ChangeHot,
NewCold is OldCold + ChangeCold,
set(hot_position, NewHot),
set(cold_position, NewCold),
update(flush),
update(water_temperature),
update(water_flow),
report([flush, tab(2), hot_position, tab(2), cold_position, tab(2),
water_temperature, tab(2), water_flow]) ]
]).
start(one, [
actions :: [
set(hot_position, 30),
set(cold_position, 30),
set(test_duration, 60),
update(water_temperature),
update(water_flow),
report([`Start test one`, tab(2), hot_position, tab(2), cold_position, tab(2),
water_temperature, tab(2), water_flow]) ]
]).
Links
http://www.depi.itch.edu.mx/apacheco/ai/fuzzy/
 Prof. Al Pacheco's programs and examples implementing fuzzy logic,
which were an inspiration for the system described in this newsletter.
http://www.karlscalculus.org/index.shtml
 Karl Hahn's excellent calculus tutor, without which I couldn't
have cleared the calculus cobwebs from my head.
http://www.quickmath.com/
 A fantastic site that Karl Hahn mentioned that does integrals
and other math functions for you.
