File Coverage

blib/lib/Algorithm/Simplex.pm
Criterion Covered Total %
statement 18 109 16.5
branch 0 16 0.0
condition n/a
subroutine 6 19 31.5
pod 7 7 100.0
total 31 151 20.5


line stmt bran cond sub pod time code
1             package Algorithm::Simplex;
2 1     1   660 use Moo;
  1         2  
  1         6  
3 1     1   1267 use MooX::Types::MooseLike::Base qw( ArrayRef HashRef Num Int Str );
  1         7591  
  1         109  
4 1     1   9 use namespace::clean;
  1         2  
  1         8  
5 1     1   249 use Carp;
  1         3  
  1         1798  
6              
7             our $VERSION = '0.43';
8              
9             has tableau => (
10             is => 'rw',
11             isa => ArrayRef [ ArrayRef [Num] ],
12             required => 1,
13             );
14              
15             has display_tableau => (
16             is => 'lazy',
17             isa => ArrayRef [ ArrayRef [Str] ],
18             );
19              
20             has objective_function_value => (
21             is => 'lazy',
22             isa => Str,
23             );
24              
25             has number_of_rows => (
26             is => 'lazy',
27             isa => Int,
28             init_arg => undef,
29             );
30              
31             has number_of_columns => (
32             is => 'lazy',
33             isa => Int,
34             init_arg => undef,
35             );
36              
37             has number_of_pivots_made => (
38             is => 'rw',
39             isa => Int,
40             default => sub { 0 },
41             );
42              
43             has EPSILON => (
44             is => 'rw',
45             isa => Num,
46             default => sub { 1e-13 },
47             );
48              
49             has MAXIMUM_PIVOTS => (
50             is => 'rw',
51             isa => Int,
52             default => sub { 200 },
53             );
54              
55             has x_variables => (
56             is => 'lazy',
57             isa => ArrayRef [ HashRef [Str] ],
58             );
59             has 'y_variables' => (
60             is => 'lazy',
61             isa => ArrayRef [ HashRef [Str] ],
62             );
63             has u_variables => (
64             is => 'lazy',
65             isa => ArrayRef [ HashRef [Str] ],
66             );
67             has v_variables => (
68             is => 'lazy',
69             isa => ArrayRef [ HashRef [Str] ],
70             );
71              
72             =head1 Name
73              
74             Algorithm::Simplex - Simplex Algorithm Implementation using Tucker Tableaux
75              
76             =head1 Synopsis
77              
78             Given a linear program formulated as a Tucker tableau, a 2D matrix or
79             ArrayRef[ArrayRef] in Perl, seek an optimal solution.
80              
81             use Algorithm::Simplex::Rational;
82             my $matrix = [
83             [ 5, 2, 30],
84             [ 3, 4, 20],
85             [10, 8, 0],
86             ];
87             my $tableau = Algorithm::Simplex::Rational->new( tableau => $matrix );
88             $tableau->solve;
89             my ($primal_solution, $dual_solution) = $tableau->current_solution;
90              
91             =head1 Methods
92              
93             =head2 _build_number_of_rows
94              
95             Set the number of rows.
96             This number represent the number of rows of the
97             coefficient matrix. It is one less than the full tableau.
98              
99             =cut
100              
101             sub _build_number_of_rows {
102 2     2   483 my $self = shift;
103              
104 2         2 return scalar @{ $self->tableau } - 1;
  2         24  
105             }
106              
107             =head2 _build_number_of_columns
108              
109             Set the number of columns given the tableau matrix.
110             This number represent the number of columns of the
111             coefficient matrix.
112              
113             =cut
114              
115             sub _build_number_of_columns {
116 2     2   485 my $self = shift;
117              
118 2         3 return scalar @{ $self->tableau->[0] } - 1;
  2         35  
119             }
120              
121             =head2 _build_x_variables
122              
123             Set x variable names for the given tableau, x1, x2 ... xn
124             These are the decision variables of the maximization problem.
125             The maximization problem is read horizontally in a Tucker tableau.
126              
127             =cut
128              
129             sub _build_x_variables {
130 0     0     my $self = shift;
131              
132 0           my $x_vars;
133 0           for my $j (0 .. $self->number_of_columns - 1) {
134 0           my $x_index = $j + 1;
135 0           $x_vars->[$j]->{'generic'} = 'x' . $x_index;
136             }
137 0           return $x_vars;
138             }
139              
140             =head2 _build_y_variables
141              
142             Set y variable names for the given tableau.
143             These are the slack variables of the maximization problem.
144              
145             =cut
146              
147             sub _build_y_variables {
148 0     0     my $self = shift;
149              
150 0           my $y_vars;
151 0           for my $i (0 .. $self->number_of_rows - 1) {
152 0           my $y_index = $i + 1;
153 0           $y_vars->[$i]->{'generic'} = 'y' . $y_index;
154             }
155 0           return $y_vars;
156             }
157              
158             =head2 _build_u_variables
159              
160             Set u variable names for the given tableau.
161             These are the slack variables of the minimization problem.
162              
163             =cut
164              
165             sub _build_u_variables {
166 0     0     my $self = shift;
167              
168 0           my $u_vars;
169 0           for my $j (0 .. $self->number_of_columns - 1) {
170 0           my $u_index = $j + 1;
171 0           $u_vars->[$j]->{'generic'} = 'u' . $u_index;
172             }
173 0           return $u_vars;
174             }
175              
176             =head2 _build_v_variables
177              
178             Set v variable names for the given tableau: v1, v2 ... vm
179             These are the decision variables for the minimization problem.
180             The minimization problem is read horizontally in a Tucker tableau.
181              
182             =cut
183              
184             sub _build_v_variables {
185 0     0     my $self = shift;
186              
187 0           my $v_vars;
188 0           for my $i (0 .. $self->number_of_rows - 1) {
189 0           my $v_index = $i + 1;
190 0           $v_vars->[$i]->{'generic'} = 'v' . $v_index;
191             }
192 0           return $v_vars;
193             }
194              
195             sub _build_display_tableau {
196 0     0     my $self = shift;
197 0           return $self->tableau;
198             }
199              
200             sub _build_objective_function_value {
201 0     0     my $self = shift;
202 0           return $self->display_tableau->[ $self->number_of_rows ]
203             ->[ $self->number_of_columns ] * (-1);
204             }
205              
206             =head2 get_bland_number_for
207              
208             Given a column number (which represents a u variable) build the bland number
209             from the generic variable name.
210              
211             =cut
212              
213             sub get_bland_number_for {
214 0     0 1   my $self = shift;
215 0           my $variable_type = shift;
216 0           my $variables = $variable_type . '_variables';
217 0           my $index = shift;
218 0           my $generic_name = $self->$variables->[$index]->{'generic'};
219              
220 0           my ($var, $num);
221 0 0         if ($generic_name =~ m{(.)(\d+)}mxs) {
222 0           $var = $1;
223 0           $num = $2;
224             }
225             else {
226 0           croak "The generic variable names have format issues.\n";
227             }
228 0 0         my $start_num =
    0          
    0          
    0          
229             $var eq 'x' ? 1
230             : $var eq 'y' ? 2
231             : $var eq 'v' ? 4
232             : $var eq 'u' ? 3
233             : croak "Variable name: $var does not equal x, y, v or u";
234 0           my $bland_number = $start_num . $num;
235 0           return $bland_number;
236             }
237              
238             =head2 determine_bland_pivot_column_number
239              
240             Find the pivot column using Bland ordering technique to prevent cycles.
241              
242             =cut
243              
244             sub determine_bland_pivot_column_number {
245 0     0 1   my ($self, @simplex_pivot_column_numbers) = @_;
246              
247 0           my @bland_number_for_simplex_pivot_column;
248 0           foreach my $col_number (@simplex_pivot_column_numbers) {
249 0           push @bland_number_for_simplex_pivot_column,
250             $self->get_bland_number_for('x', $col_number);
251             }
252              
253             # Pass blands number to routine that returns index of location where minimum bland occurs.
254             # Use this index to return the bland column column number from @positive_profit_column_numbers
255 0           my @bland_column_number_index =
256             $self->min_index(\@bland_number_for_simplex_pivot_column);
257 0           my $bland_column_number_index = $bland_column_number_index[0];
258              
259 0           return $simplex_pivot_column_numbers[$bland_column_number_index];
260             }
261              
262             =head2 determine_bland_pivot_row_number
263              
264             Find the pivot row using Bland ordering technique to prevent cycles.
265              
266             =cut
267              
268             sub determine_bland_pivot_row_number {
269 0     0 1   my ($self, $positive_ratios, $positive_ratio_row_numbers) = @_;
270              
271             # Now that we have the ratios and their respective rows we can find the min
272             # and then select the lowest bland min if there are ties.
273 0           my @min_indices = $self->min_index($positive_ratios);
274 0           my @min_ratio_row_numbers =
275 0           map { $positive_ratio_row_numbers->[$_] } @min_indices;
276 0           my @bland_number_for_min_ratio_rows;
277 0           foreach my $row_number (@min_ratio_row_numbers) {
278 0           push @bland_number_for_min_ratio_rows,
279             $self->get_bland_number_for('y', $row_number);
280             }
281              
282             # Pass blands number to routine that returns index of location where minimum bland occurs.
283             # Use this index to return the bland row number.
284 0           my @bland_min_ratio_row_index =
285             $self->min_index(\@bland_number_for_min_ratio_rows);
286 0           my $bland_min_ratio_row_index = $bland_min_ratio_row_index[0];
287 0           return $min_ratio_row_numbers[$bland_min_ratio_row_index];
288             }
289              
290             =head2 min_index
291              
292             Determine the index of the element with minimal value.
293             Used when finding bland pivots.
294              
295             =cut
296              
297             sub min_index {
298 0     0 1   my ($self, $l) = @_;
299 0           my $n = @{$l};
  0            
300 0 0         if (!$n) {
301 0           return ();
302             }
303 0           my $v_min = $l->[0];
304 0           my @i_min = (0);
305              
306 0           for my $i (1 .. $n - 1) {
307 0 0         if ($l->[$i] < $v_min) {
    0          
308 0           $v_min = $l->[$i];
309 0           @i_min = ($i);
310             }
311             elsif ($l->[$i] == $v_min) {
312 0           push @i_min, $i;
313             }
314             }
315 0           return @i_min;
316              
317             }
318              
319             =head2 exchange_pivot_variables
320              
321             Exchange the variables when a pivot is done. The method pivot() does the
322             algrebra while this method does the variable swapping, and thus tracking of
323             what variables take on non-zero values. This is needed to accurately report
324             an optimal solution.
325              
326             =cut
327              
328             sub exchange_pivot_variables {
329 0     0 1   my $self = shift;
330 0           my $pivot_row_number = shift;
331 0           my $pivot_column_number = shift;
332              
333             # exchange variables based on $pivot_column_number and $pivot_row_number
334 0           my $increasing_primal_variable = $self->x_variables->[$pivot_column_number];
335 0           my $zeroeing_primal_variable = $self->y_variables->[$pivot_row_number];
336 0           $self->x_variables->[$pivot_column_number] = $zeroeing_primal_variable;
337 0           $self->y_variables->[$pivot_row_number] = $increasing_primal_variable;
338              
339 0           my $increasing_dual_variable = $self->v_variables->[$pivot_row_number];
340 0           my $zeroeing_dual_variable = $self->u_variables->[$pivot_column_number];
341 0           $self->v_variables->[$pivot_row_number] = $zeroeing_dual_variable;
342 0           $self->u_variables->[$pivot_column_number] = $increasing_dual_variable;
343 0           return;
344             }
345              
346             =head2 get_row_and_column_numbers
347              
348             Get the dimensions of the tableau.
349              
350             =cut
351              
352             sub get_row_and_column_numbers {
353 0     0 1   my $self = shift;
354 0           return $self->number_of_rows, $self->number_of_columns;
355             }
356              
357             =head2 determine_bland_pivot_row_and_column_numbers
358              
359             Higher level function that uses others to return the (bland) pivot point.
360              
361             =cut
362              
363             sub determine_bland_pivot_row_and_column_numbers {
364 0     0 1   my $self = shift;
365              
366 0           my @simplex_pivot_columns = $self->determine_simplex_pivot_columns;
367 0           my $pivot_column_number =
368             $self->determine_bland_pivot_column_number(@simplex_pivot_columns);
369 0           my ($positive_ratios, $positive_ratio_row_numbers) =
370             $self->determine_positive_ratios($pivot_column_number);
371 0           my $pivot_row_number =
372             $self->determine_bland_pivot_row_number($positive_ratios,
373             $positive_ratio_row_numbers);
374              
375 0           return ($pivot_row_number, $pivot_column_number);
376             }
377              
378             1;
379              
380             __END__