NAME

Simulation::DiscreteEvent::Cookbook::Callcenter - modelling call center

MODELLING CALL CENTER

Model Description

We will assume that call center has N channels. When client calls to center he takes one of the available channels in random way. The channel stays busy till client disconnected. All channels are equal, so we have to trace only the number of busy channels, but not which channels are used. If all channels are busy when client calling, he gets rejected. We assume that intervals between incoming calls are exponentially distributed with pdf f(t)=lambda*exp(-lambda*t), t>0. The lambda is the number of incoming calls in a unit of time. We assume that call duration times are lognormally distributed with pdf f(t)=(1/(t*sigma*sqrt(2*pi)))*exp(-(ln(t)-mu)^2/(2*sigma^2)), t>0. Average call duration is exp(mu + sigma^2/2).

Building a Model

In Simulation::DiscreteEvent::Cookbook::MM10 we used one server class. Now let's split system onto three parts -- generator, call center, and sink. Generator will generate new call events. Call center will handle calls. And sink will receive and count served and rejected calls. So the flow will be as follows:

Generator --> Call center --> Sink

Generator

Generator should generate "new_call" events for call center in random moments of time. Every time it sends "new_call" to call center it generates random interval and schedules for itself event when it will send next call to call center. Here's the code:

package Simulation::DiscreteEvent::CB::Generator;

use Moose;
BEGIN { extends 'Simulation::DiscreteEvent::Server' }

use Math::Random qw(random_exponential);

has rate => ( is => 'rw', isa => 'Num', default => 1 );
has dest => ( is => 'rw', isa => 'Simulation::DiscreteEvent::Server' );

sub next : Event {
    my $self = shift;
    $self->model->send( $self->dest, 'new_call' );
    my $next_time = $self->model->time 
        + random_exponential( 1, 1 / $self->rate );
    $self->model->schedule( $next_time, $self, 'next' );
}

no Moose;
__PACKAGE__->meta->make_immutable;

Doesn't that look like a pretty typical task? That's right, and there's Simulation::DiscreteEvent::Generator module for this.

Call Center

Call center serves incoming calls. When new call received we should check if there's free channel, increase number of busy channels, and schedule event when we finish serving this call. After call is served, or if it is rejected because all channels are busy, we're sending it to the sink object.

In M/M/1/0 simulation we just counted numbers of served and rejected calls, but now we also want to get average load of the call center, i.e. average number of busy channels. For this purpose we will use Simulation::DiscreteEvent::NumericState role.

Here's the Callcenter's code:

package Simulation::DiscreteEvent::CB::Callcenter;
use Moose;
BEGIN { extends 'Simulation::DiscreteEvent::Server' }
with 'Simulation::DiscreteEvent::NumericState';
use Math::Random qw(random_normal);

has mu       => ( is => 'rw', isa => 'Num', default => 3 );
has sigma    => ( is => 'rw', isa => 'Num', default => 1 );
has channels => ( is => 'rw', isa => 'Int', default => 10 );
has dest => (
    is  => 'rw',
    isa => 'Simulation::DiscreteEvent::Server'
);

# generate random call duration
sub _call_duration {
    my $self = shift;
    my $n = random_normal( 1, 0, 1 );
    exp( $self->mu + $self->sigma * $n );
}

sub new_call : Event {
    my $self = shift;

    # if there's a free channel serve the call
    if ( $self->state < $self->channels ) {
        $self->state_inc;
        my $finish_time = $self->model->time + $self->_call_duration;
        $self->model->schedule( $finish_time, $self, 'served' );
    }

    # otherwise reject call
    else {
        $self->model->send( $self->dest, 'rejected' );
    }
}

sub served : Event {
    my $self = shift;
    $self->state_dec;
    $self->model->send( $self->dest, 'served' );
}

no Moose;
__PACKAGE__->meta->make_immutable;

Note $self->state method. It is provided by NumericState role. It may be used for any server which state is described by a number. state method allows you to get or set current state (the number of busy channels for Callcenter), and state_inc, state_dec methods increase or decrease number of busy channels. NumericState role automatically collects all state changes and is able to compute some general parameters from collected data.

Sink

This object receives all calls after they were served or rejected. The only purpose of this object is to count numbers of served and rejected calls. We could do it in Callcenter object, but using separate object will allow us to demonstrate using of Simulation::DiscreteEvent::Recorder role. This role automatically records all events received by object and provides you with some functions to access collected data. The Sink class is quite simple:

package Simulation::DiscreteEvent::CB::Sink;

use Moose;
BEGIN { extends 'Simulation::DiscreteEvent::Server' }
with 'Simulation::DiscreteEvent::Recorder';

sub served : Event {}
sub rejected : Event {}

no Moose;
__PACKAGE__->meta->make_immutable;

As you may see the only thing we need to do here is to define allowed events.

Combining All Pieces

And now let's combine all these modules together:

use Simulation::DiscreteEvent;

my $model = Simulation::DiscreteEvent->new;

# add servers to model
my $gen   = $model->add('Simulation::DiscreteEvent::CB::Generator');
my $cc    = $model->add(
    'Simulation::DiscreteEvent::CB::Callcenter',
    mu       => 3.5,
    sigma    => 1,
    channels => 50,
);
my $sink = $model->add('Simulation::DiscreteEvent::CB::Sink');

# connect servers
$gen->dest($cc);
$cc->dest($sink);

# run simulation
$model->send( $gen, 'next' );
$model->run(1000);

# these functions are provided by S::DE::Recorder
my $served   = $sink->get_number_of('served');
my $rejected = $sink->get_number_of('rejected');

# output results
print "Model time:   ", $model->time, "\n";
print "Calls total:  ", $served + $rejected, "\n";
print "Served:       $served\n";
print "Rejected:     $rejected\n";
print "Average load: ", $cc->average_load, "\n";

And here's the example output from the script:

Model time:   1000
Calls total:  959
Served:       886
Rejected:     73
Average load: 43.2777190672934

Note using of get_number_of method to get number of recorded "served" and "rejected" events. This method is provided by Simulation::DiscreteEvent::Recorder role, and returns how many times object has received specified event. Also, note using of average_load method to get average number of busy channels. This method is provided by Simulation::DiscreteEvent::NumericState role.

AUTHOR

Pavel Shaydo, <zwon at cpan.org>

BUGS

Please report any bugs or feature requests to bug-simulation-discreteevent at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Simulation-DiscreteEvent. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SEE ALSO

Simulation::DiscreteEvent

LICENSE AND COPYRIGHT

Copyright 2010 Pavel Shaydo.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.