Hi all,

I wrote an article entitled 'Fuzzy Logic in Perl' that will appear in the
June issue of The Perl Journal (www.tpj.com) - due out next week or the
one after. In it, I describe how to design a fuzzy controller in pure Perl
using my AI::FuzzyInference module (available from CPAN) to balance a ball
on a rod and prevent it from falling.

Attached is a slightly more elaborate version of the program I describe in
the article (released with permission). You need the AI::FuzzyInference
and Tk modules to run it. I use Tk to show the state of the rod and ball
at each time step.

I'm sure some people on this list might be interested. I would appreciate
any feedback you might have on the module. I would also be interested in
hearing from anyone who uses it.

Thanks,
--Ala
#!perl -w

use strict;
use Tk;
use Tk::LabEntry;
use Tk::Pane;
use AI::FuzzyInference;

use constant PI => 3.1415927;
use constant G  => 9.81;

my $halfLenRod   = 5;
my $timeStep     = 0.05;

my $thRod;    # between -30 and 30 degrees.
my $velBall;  # ball's velocity. From -15 .. 15 m/s
my $posBall;  # ball's position. From -5 .. 5 m
my $dTheta;
my $time = 0;

# initialize.
$thRod   = -10;
$velBall = -2;
$posBall = 4;
$dTheta  = 0;

# create the FIS.
my $fis = new AI::FuzzyInference;

# define the input variables.
$fis->inVar(posBall  => -5, 5,
            far_left  => [-4, 1, -2, 0],
            left      => [-4, 0, -2, 1, 0, 0],
            center    => [-2, 0, 0, 1, 2, 0],
            right     => [0, 0, 2, 1, 4, 0],
            far_right => [2, 0, 4, 1],
            );

$fis->inVar(velBall   => -15, 15,
            fast_neg   => [-9, 1, -3, 0],
            medium_neg => [-9, 0, -3, 1, 0, 0],
            slow       => [-3, 0, 0, 1, 3, 0],
            medium_pos => [0, 0, 3, 1, 9, 0],
            fast_pos   => [3, 0, 9, 1],
            );

$fis->inVar(thRod      => -30, 30,
            large_neg  => [-20, 1, -10, 0],
            medium_neg => [-20, 0, -10, 1, 0, 0],
            small      => [-10, 0, 0, 1, 10, 0],
            medium_pos => [0, 0, 10, 1, 20, 0],
            large_pos  => [10, 0, 20, 1],
            );

# define the output variable.
$fis->outVar(dTheta    => -10, 10,
             large_neg => [-8, 1, -4, 0],
             small_neg => [-8, 0, -4, 1, 0, 0],
             zero      => [-4, 0, 0, 1, 4, 0],
             small_pos => [0, 0, 4, 1, 8, 0],
             large_pos => [4, 0, 8, 1],
             );

# now define the rules.
$fis->addRule(
              'posBall=far_left     & velBall=fast_neg     & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=fast_neg     & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=fast_neg     & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=far_left     & velBall=fast_neg     & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=far_left     & velBall=fast_neg     & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=far_left     & velBall=medium_neg   & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=medium_neg   & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=medium_neg   & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=far_left     & velBall=medium_neg   & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=far_left     & velBall=medium_neg   & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=far_left     & velBall=slow         & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=slow         & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=slow         & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=far_left     & velBall=slow         & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=far_left     & velBall=slow         & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=far_left     & velBall=medium_pos   & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=far_left     & velBall=medium_pos   & thRod=medium_neg  ' => 
'dTheta=small_pos',
              'posBall=far_left     & velBall=medium_pos   & thRod=small       ' => 
'dTheta=zero',
              'posBall=far_left     & velBall=medium_pos   & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=far_left     & velBall=medium_pos   & thRod=large_pos   ' => 
'dTheta=small_neg',

              'posBall=far_left     & velBall=fast_pos     & thRod=large_neg   ' => 
'dTheta=small_pos',
              'posBall=far_left     & velBall=fast_pos     & thRod=medium_neg  ' => 
'dTheta=small_pos',
              'posBall=far_left     & velBall=fast_pos     & thRod=small       ' => 
'dTheta=zero',
              'posBall=far_left     & velBall=fast_pos     & thRod=medium_pos  ' => 
'dTheta=small_neg',
              'posBall=far_left     & velBall=fast_pos     & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=left         & velBall=fast_neg     & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=fast_neg     & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=fast_neg     & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=left         & velBall=fast_neg     & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=left         & velBall=fast_neg     & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=left         & velBall=medium_neg   & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=medium_neg   & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=medium_neg   & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=left         & velBall=medium_neg   & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=left         & velBall=medium_neg   & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=left         & velBall=slow         & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=slow         & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=slow         & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=left         & velBall=slow         & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=left         & velBall=slow         & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=left         & velBall=medium_pos   & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=left         & velBall=medium_pos   & thRod=medium_neg  ' => 
'dTheta=small_pos',
              'posBall=left         & velBall=medium_pos   & thRod=small       ' => 
'dTheta=zero',
              'posBall=left         & velBall=medium_pos   & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=left         & velBall=medium_pos   & thRod=large_pos   ' => 
'dTheta=small_neg',

              'posBall=left         & velBall=fast_pos     & thRod=large_neg   ' => 
'dTheta=small_pos',
              'posBall=left         & velBall=fast_pos     & thRod=medium_neg  ' => 
'dTheta=small_pos',
              'posBall=left         & velBall=fast_pos     & thRod=small       ' => 
'dTheta=zero',
              'posBall=left         & velBall=fast_pos     & thRod=medium_pos  ' => 
'dTheta=small_neg',
              'posBall=left         & velBall=fast_pos     & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=center       & velBall=fast_neg     & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=center       & velBall=fast_neg     & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=center       & velBall=fast_neg     & thRod=small       ' => 
'dTheta=large_pos',
              'posBall=center       & velBall=fast_neg     & thRod=medium_pos  ' => 
'dTheta=small_pos',
              'posBall=center       & velBall=fast_neg     & thRod=large_pos   ' => 
'dTheta=small_pos',

              'posBall=center       & velBall=medium_neg   & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=center       & velBall=medium_neg   & thRod=medium_neg  ' => 
'dTheta=large_pos',
              'posBall=center       & velBall=medium_neg   & thRod=small       ' => 
'dTheta=small_pos',
              'posBall=center       & velBall=medium_neg   & thRod=medium_pos  ' => 
'dTheta=small_pos',
              'posBall=center       & velBall=medium_neg   & thRod=large_pos   ' => 
'dTheta=zero',

              'posBall=center       & velBall=slow         & thRod=large_neg   ' => 
'dTheta=small_pos',
              'posBall=center       & velBall=slow         & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=center       & velBall=slow         & thRod=small       ' => 
'dTheta=zero',
              'posBall=center       & velBall=slow         & thRod=medium_pos  ' => 
'dTheta=zero',
              'posBall=center       & velBall=slow         & thRod=large_pos   ' => 
'dTheta=small_neg',

              'posBall=center       & velBall=medium_pos   & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=center       & velBall=medium_pos   & thRod=medium_neg  ' => 
'dTheta=small_neg',
              'posBall=center       & velBall=medium_pos   & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=center       & velBall=medium_pos   & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=center       & velBall=medium_pos   & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=center       & velBall=fast_pos     & thRod=large_neg   ' => 
'dTheta=small_neg',
              'posBall=center       & velBall=fast_pos     & thRod=medium_neg  ' => 
'dTheta=small_neg',
              'posBall=center       & velBall=fast_pos     & thRod=small       ' => 
'dTheta=large_neg',
              'posBall=center       & velBall=fast_pos     & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=center       & velBall=fast_pos     & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=right        & velBall=fast_neg     & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=right        & velBall=fast_neg     & thRod=medium_neg  ' => 
'dTheta=small_pos',
              'posBall=right        & velBall=fast_neg     & thRod=small       ' => 
'dTheta=zero',
              'posBall=right        & velBall=fast_neg     & thRod=medium_pos  ' => 
'dTheta=small_neg',
              'posBall=right        & velBall=fast_neg     & thRod=large_pos   ' => 
'dTheta=small_neg',

              'posBall=right        & velBall=medium_neg   & thRod=large_neg   ' => 
'dTheta=small_pos',
              'posBall=right        & velBall=medium_neg   & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=right        & velBall=medium_neg   & thRod=small       ' => 
'dTheta=zero',
              'posBall=right        & velBall=medium_neg   & thRod=medium_pos  ' => 
'dTheta=small_neg',
              'posBall=right        & velBall=medium_neg   & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=right        & velBall=slow         & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=right        & velBall=slow         & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=right        & velBall=slow         & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=right        & velBall=slow         & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=right        & velBall=slow         & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=right        & velBall=medium_pos   & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=right        & velBall=medium_pos   & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=right        & velBall=medium_pos   & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=right        & velBall=medium_pos   & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=right        & velBall=medium_pos   & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=right        & velBall=fast_pos     & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=right        & velBall=fast_pos     & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=right        & velBall=fast_pos     & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=right        & velBall=fast_pos     & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=right        & velBall=fast_pos     & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=far_right    & velBall=fast_neg     & thRod=large_neg   ' => 
'dTheta=large_pos',
              'posBall=far_right    & velBall=fast_neg     & thRod=medium_neg  ' => 
'dTheta=small_pos',
              'posBall=far_right    & velBall=fast_neg     & thRod=small       ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=fast_neg     & thRod=medium_pos  ' => 
'dTheta=small_neg',
              'posBall=far_right    & velBall=fast_neg     & thRod=large_pos   ' => 
'dTheta=small_neg',

              'posBall=far_right    & velBall=medium_neg   & thRod=large_neg   ' => 
'dTheta=small_pos',
              'posBall=far_right    & velBall=medium_neg   & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=medium_neg   & thRod=small       ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=medium_neg   & thRod=medium_pos  ' => 
'dTheta=small_neg',
              'posBall=far_right    & velBall=medium_neg   & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=far_right    & velBall=slow         & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=slow         & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=slow         & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=far_right    & velBall=slow         & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=far_right    & velBall=slow         & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=far_right    & velBall=medium_pos   & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=medium_pos   & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=medium_pos   & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=far_right    & velBall=medium_pos   & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=far_right    & velBall=medium_pos   & thRod=large_pos   ' => 
'dTheta=large_neg',

              'posBall=far_right    & velBall=fast_pos     & thRod=large_neg   ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=fast_pos     & thRod=medium_neg  ' => 
'dTheta=zero',
              'posBall=far_right    & velBall=fast_pos     & thRod=small       ' => 
'dTheta=small_neg',
              'posBall=far_right    & velBall=fast_pos     & thRod=medium_pos  ' => 
'dTheta=large_neg',
              'posBall=far_right    & velBall=fast_pos     & thRod=large_pos   ' => 
'dTheta=large_neg',
              );

drawGUI();

MainLoop;

# this subroutine calculates the new values of the ball's position
# and velocity after a period of time $timeStep.
# Friction is not modeled.
sub calcNewData {
  my $acc  = G * sin ($thRod * PI / 180);
  my $Vnew = $velBall + $acc * $timeStep;
  my $dist = $velBall * $timeStep + 0.5 * $acc * $timeStep * $timeStep;

  $velBall  = $Vnew;
  $posBall += $dist;

  $velBall =  15 if $velBall >  15;
  $velBall = -15 if $velBall < -15;

  $time += $timeStep;
}

# This sub draws the gui.
sub drawGUI {
   my $mw = new MainWindow;

   my $canvas = $mw->Canvas(qw/-bg black -height 400 -width 600/)->pack;

   $canvas->createLine(0, 0, 0, 0,   qw/-width 2 -fill white -tags ROD/);
   $canvas->createOval(0, 0, 50, 50, qw/-fill green -tags BALL/);

   my $f = $mw->Frame->pack(qw/-fill x/);

   my @rules;

   my $id;

   $f->Button(-text    => 'run',
              -command => sub {
                $id = $canvas->repeat(100 => sub {
                                        # update the ball's data.
                                        calcNewData();

                                        # check for termination conditions.

                                        # stop if ball is almost stationary and the rod
                                        # is almost flat.
                                        if (abs($velBall) < 0.005 && abs($thRod) < 
0.001) {
                                          print "Simulation ended.\n";
                                          $canvas->afterCancel($id);
                                          return;
                                        }

                                        # stop if ball fell off the rod.
                                        if ($posBall > $halfLenRod or $posBall < 
-$halfLenRod) {
                                          print "Ball fell off the rod!\n";
                                          $canvas->afterCancel($id);
                                          return;
                                        }

                                        # compute the new angle of the rod.
                                        $fis->compute(posBall => $posBall,
                                                      velBall => $velBall,
                                                      thRod   => $thRod);

                                        $dTheta = $fis->value('dTheta');
                                        $thRod += $dTheta;

                                        $thRod = -30 if $thRod < -30;
                                        $thRod =  30 if $thRod >  30;

                                        # don't do this.
                                        # I'm peaking inside the $fis object.
                                        for my $i (@{$fis->{FIRED}}) {
                                            $rules[$i->[0]] = $i->[1];
                                        }
                                        $mw->update;
                                        # update our drawing.
                                        updateCanvas($canvas);
                                      });
              })->pack(qw/side left -ipadx 10/);

   $f->Button(-text    => 'pause',
              -command => sub { $canvas->afterCancel($id) })->pack(qw/-side left/);

   $f->LabEntry(-label => 'Ball Pos',
                -textvariable => \$posBall,
               )->pack(qw/-side left -padx 10/);

   $f->LabEntry(-label => 'Ball Speed',
                -textvariable => \$velBall,
               )->pack(qw/-side left -padx 10/);

   $f->LabEntry(-label => 'Rod Angle',
                -textvariable => \$thRod,
               )->pack(qw/-side left -padx 10/);

   $f->LabEntry(-label => 'dTheta',
                -textvariable => \$dTheta,
               )->pack(qw/-side left -padx 10/);

   my $f2 = $mw->Scrolled(qw/Pane -sticky new/)->pack(qw/-fill both -expand 1/);

   my $ind = 0;
   for my $r (@{$fis->{RULES}}) { # peek inside
       $f2->LabEntry(-label => "$r->[0] => $r->[1]",
                     -textvariable => \$rules[$ind],
                     -labelPack => [qw/-side right/],
                     )->pack(qw/-fill none -side top -anchor w/);
       $ind++;
   }

   updateCanvas($canvas);
}

# This subroutine draws the rod at its current angle, and
# the ball at its current position.
sub updateCanvas {
  my $c = shift;

  my $ly = 200;
  my $dy = int(40 * $halfLenRod * tan(PI * $thRod / 180));
  $c->coords(ROD => 100, $ly - $dy, 500, $ly + $dy);

  my $by = 150 + $posBall * $dy / $halfLenRod;

  my $bx = 100 + (5 + $posBall) * 40;
  $c->coords(BALL => $bx - 25, $by, $bx + 25, $by + 50);
}

# tangent sub.
sub tan { sin($_[0]) / cos($_[0])  }

Reply via email to