File Coverage

blib/lib/Algorithm/Simplex.pm
Criterion Covered Total %
statement 101 109 92.6
branch 9 16 56.2
condition n/a
subroutine 16 19 84.2
pod 7 7 100.0
total 133 151 88.0


line stmt bran cond sub pod time code
1             package Algorithm::Simplex;
2 3     3   1355 use Moo;
  3         7  
  3         13  
3 3     3   2091 use MooX::Types::MooseLike::Base qw( ArrayRef HashRef Num Int Str );
  3         17302  
  3         218  
4 3     3   19 use namespace::clean;
  3         7  
  3         15  
5 3     3   781 use Carp;
  3         6  
  3         4119  
6              
7             our $VERSION = '0.44';
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 26     26   672 my $self = shift;
103              
104 26         44 return scalar @{ $self->tableau } - 1;
  26         407  
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 26     26   352 my $self = shift;
117              
118 26         44 return scalar @{ $self->tableau->[0] } - 1;
  26         424  
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 12     12   117 my $self = shift;
131              
132 12         18 my $x_vars;
133 12         168 for my $j (0 .. $self->number_of_columns - 1) {
134 40         125 my $x_index = $j + 1;
135 40         101 $x_vars->[$j]->{'generic'} = 'x' . $x_index;
136             }
137 12         207 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 12     12   115 my $self = shift;
149              
150 12         46 my $y_vars;
151 12         175 for my $i (0 .. $self->number_of_rows - 1) {
152 50         146 my $y_index = $i + 1;
153 50         115 $y_vars->[$i]->{'generic'} = 'y' . $y_index;
154             }
155 12         189 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 12     12   124 my $self = shift;
167              
168 12         16 my $u_vars;
169 12         165 for my $j (0 .. $self->number_of_columns - 1) {
170 40         122 my $u_index = $j + 1;
171 40         122 $u_vars->[$j]->{'generic'} = 'u' . $u_index;
172             }
173 12         182 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 12     12   152 my $self = shift;
186              
187 12         19 my $v_vars;
188 12         182 for my $i (0 .. $self->number_of_rows - 1) {
189 50         154 my $v_index = $i + 1;
190 50         131 $v_vars->[$i]->{'generic'} = 'v' . $v_index;
191             }
192 12         198 return $v_vars;
193             }
194              
195             sub _build_display_tableau {
196 0     0   0 my $self = shift;
197 0         0 return $self->tableau;
198             }
199              
200             sub _build_objective_function_value {
201 0     0   0 my $self = shift;
202 0         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 120     120 1 159 my $self = shift;
215 120         149 my $variable_type = shift;
216 120         177 my $variables = $variable_type . '_variables';
217 120         142 my $index = shift;
218 120         1794 my $generic_name = $self->$variables->[$index]->{'generic'};
219              
220 120         4071 my ($var, $num);
221 120 50       308 if ($generic_name =~ m{(.)(\d+)}mxs) {
222 120         209 $var = $1;
223 120         180 $num = $2;
224             }
225             else {
226 0         0 croak "The generic variable names have format issues.\n";
227             }
228 120 0       218 my $start_num =
    0          
    50          
    100          
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 120         207 my $bland_number = $start_num . $num;
235 120         277 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 40     40 1 83 my ($self, @simplex_pivot_column_numbers) = @_;
246              
247 40         53 my @bland_number_for_simplex_pivot_column;
248 40         68 foreach my $col_number (@simplex_pivot_column_numbers) {
249 74         147 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 40         100 my @bland_column_number_index =
256             $self->min_index(\@bland_number_for_simplex_pivot_column);
257 40         68 my $bland_column_number_index = $bland_column_number_index[0];
258              
259 40         81 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 40     40 1 78 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 40         83 my @min_indices = $self->min_index($positive_ratios);
274             my @min_ratio_row_numbers =
275 40         67 map { $positive_ratio_row_numbers->[$_] } @min_indices;
  46         100  
276 40         58 my @bland_number_for_min_ratio_rows;
277 40         65 foreach my $row_number (@min_ratio_row_numbers) {
278 46         118 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 40         90 my @bland_min_ratio_row_index =
285             $self->min_index(\@bland_number_for_min_ratio_rows);
286 40         61 my $bland_min_ratio_row_index = $bland_min_ratio_row_index[0];
287 40         81 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 120     120 1 179 my ($self, $l) = @_;
299 120         140 my $n = @{$l};
  120         164  
300 120 50       210 if (!$n) {
301 0         0 return ();
302             }
303 120         177 my $v_min = $l->[0];
304 120         183 my @i_min = (0);
305              
306 120         215 for my $i (1 .. $n - 1) {
307 118 100       296 if ($l->[$i] < $v_min) {
    100          
308 24         35 $v_min = $l->[$i];
309 24         40 @i_min = ($i);
310             }
311             elsif ($l->[$i] == $v_min) {
312 6         9 push @i_min, $i;
313             }
314             }
315 120         233 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 40     40 1 160 my $self = shift;
330 40         57 my $pivot_row_number = shift;
331 40         49 my $pivot_column_number = shift;
332              
333             # exchange variables based on $pivot_column_number and $pivot_row_number
334 40         538 my $increasing_primal_variable = $self->x_variables->[$pivot_column_number];
335 40         720 my $zeroeing_primal_variable = $self->y_variables->[$pivot_row_number];
336 40         708 $self->x_variables->[$pivot_column_number] = $zeroeing_primal_variable;
337 40         719 $self->y_variables->[$pivot_row_number] = $increasing_primal_variable;
338              
339 40         752 my $increasing_dual_variable = $self->v_variables->[$pivot_row_number];
340 40         2379 my $zeroeing_dual_variable = $self->u_variables->[$pivot_column_number];
341 40         2172 $self->v_variables->[$pivot_row_number] = $zeroeing_dual_variable;
342 40         681 $self->u_variables->[$pivot_column_number] = $increasing_dual_variable;
343 40         252 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 0 my $self = shift;
354 0         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 40     40 1 104 my $self = shift;
365              
366 40         99 my @simplex_pivot_columns = $self->determine_simplex_pivot_columns;
367 40         112 my $pivot_column_number =
368             $self->determine_bland_pivot_column_number(@simplex_pivot_columns);
369 40         111 my ($positive_ratios, $positive_ratio_row_numbers) =
370             $self->determine_positive_ratios($pivot_column_number);
371 40         104 my $pivot_row_number =
372             $self->determine_bland_pivot_row_number($positive_ratios,
373             $positive_ratio_row_numbers);
374              
375 40         128 return ($pivot_row_number, $pivot_column_number);
376             }
377              
378             1;
379              
380             __END__