File Coverage

blib/lib/Math/LP.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Math::LP;
2 4     4   2849 use strict;
  4         8  
  4         132  
3 4     4   22 use Exporter;
  4         7  
  4         142  
4 4     4   2856 use Math::LP::Constraint;
  0            
  0            
5             use Math::LP::Solve;
6             our(@EXPORT,@EXPORT_OK,%EXPORT_TAGS, # Exporter related
7             $MAX,$MIN, # objective function types
8             $OPTIMAL,$MILP_FAIL,$INFEASIBLE,$UNBOUNDED,$FAILURE,$RUNNING,
9             $FEAS_FOUND,$NO_FEAS_FOUND,$BREAK_BB, # possible status values after solve
10             $VERSION,
11             );
12             use base qw(Math::LP::Object Exporter);
13             use fields (
14             'solver_status', # status after solve() is called
15             'variables', # hash of variables used in this LP
16             'constraints', # array of constraints in this LP
17             'objective_function', # Math::LP::LinearCombination object
18             'type', # either $MAX or $MIN
19             '_dbuf', # doublePtr (wrapper for double*) buffer used internally for passing data to lprec structs
20             '_dbufsize', # and its size
21             'lprec', # the lprec structure used internally by the solver
22             # (Math::LP::Solve)
23             );
24             $VERSION = '0.03';
25              
26             # only the package variables are made available for exporting
27             @EXPORT = qw();
28             %EXPORT_TAGS = (
29             types => [qw($MAX $MIN)],
30             solver_status => [qw($OPTIMAL $MILP_FAIL $INFEASIBLE $UNBOUNDED $RUNNING
31             $FEAS_FOUND $NO_FEAS_FOUND $BREAK_BB)],
32             );
33             $EXPORT_TAGS{all} = [@{$EXPORT_TAGS{types}},
34             @{$EXPORT_TAGS{solver_status}}];
35             @EXPORT_OK = @{$EXPORT_TAGS{all}};
36              
37             BEGIN {
38             # objective function types (not defined by LP_Solve)
39             *MAX = \0;
40             *MIN = \1;
41            
42             # solver states (exit states of LP_Solve's solve() and lag_solve())
43             *OPTIMAL = \$Math::LP::Solve::OPTIMAL;
44             *MILP_FAIL = \$Math::LP::Solve::MILP_FAIL;
45             *INFEASIBLE = \$Math::LP::Solve::INFEASIBLE;
46             *UNBOUNDED = \$Math::LP::Solve::UNBOUNDED;
47             *FAILURE = \$Math::LP::Solve::FAILURE;
48             *RUNNING = \$Math::LP::Solve::RUNNING;
49             *FEAS_FOUND = \$Math::LP::Solve::FEAS_FOUND;
50             *NO_FEAS_FOUND = \$Math::LP::Solve::NO_FEAS_FOUND;
51             *BREAK_BB = \$Math::LP::Solve::BREAK_BB;
52             }
53              
54             ### Object setup
55             sub initialize {
56             my Math::LP $this = shift;
57             $this->{variables} ||= {};
58             $this->{constraints} ||= [];
59             $this->{_dbufsize} ||= 32;
60             $this->{_dbuf} = Math::LP::Solve::ptrcreate('double',0.0,$this->{_dbufsize});
61             }
62             sub DESTROY {
63             my Math::LP $this = shift;
64             Math::LP::Solve::ptrfree($this->{_dbuf});
65             }
66              
67             ### Memory handling
68             sub get_dbuf { # with dynamic buffer management
69             my Math::LP $this = shift;
70             my $size = shift;
71             my $initval = shift || 0.0;
72              
73             # update buffer size if needed
74             if($this->{_dbufsize} < $size) {
75             Math::LP::Solve::ptrfree($this->{_dbuf});
76             while($this->{_dbufsize} < $size) {
77             $this->{_dbufsize} *= 2;
78             }
79             $this->{_dbuf} = Math::LP::Solve::ptrcreate('double',0.0,$this->{_dbufsize});
80             }
81              
82             # initialize the buffer
83             for(my $i = 0; $i < $size; ++$i) {
84             Math::LP::Solve::ptrset($this->{_dbuf},$initval,$i);
85             }
86            
87             return $this->{_dbuf};
88             }
89              
90             ### Manipulation of the LP
91             sub nr_rows { # == nr constraints
92             my Math::LP $this = shift;
93             return scalar @{$this->{constraints}};
94             }
95             sub nr_cols { # == nr variables
96             my Math::LP $this = shift;
97             return scalar keys %{$this->{variables}};
98             }
99             sub add_variable { # assigns an index to new variables, returns the variable's index
100             my Math::LP $this = shift;
101             my Math::LP::Variable $var = shift;
102             if(exists $this->{variables}->{$var->{name}}) { # already registered variable
103             defined $var->{col_index} or
104             $this->croak("A variable named `" . $var->{name} . "' has already been registered to the LP,\n"
105             . "It seems though that the variable you are currently trying to register\n"
106             . "differs from the already registered one (as col_index is undefined). Stopped"
107             );
108             }
109             else { # new variable
110             $this->{variables}->{$var->{name}} = $var; # registers the variable
111             $var->{col_index} = $this->nr_cols(); # first variable gets 1, second 2, ...
112             }
113             return $var->{col_index};
114             }
115             sub add_constraint { # does what it says, implicitly adds all the variables
116             my Math::LP $this = shift;
117             my Math::LP::Constraint $constr = shift;
118              
119             # register all variables present in the constraint
120             $this->add_variable($_) foreach @{$constr->{lhs}->get_variables()};
121              
122             # register the constraint
123             push @{$this->{constraints}}, $constr;
124             $constr->{row_index} = $this->nr_rows();
125              
126             return $constr->{row_index};
127             }
128             sub set_objective_function {
129             my Math::LP $this = shift;
130              
131             # initialize the objective function and type
132             my $obj_fn = $this->{objective_function} = shift;
133             $this->{type} = shift;
134            
135             # register all variables in the objective function
136             $this->add_variable($_) foreach @{$obj_fn->get_variables()};
137             }
138             sub maximize_for {
139             $_[0]->set_objective_function($_[1],$MAX);
140             }
141             sub minimize_for {
142             $_[0]->set_objective_function($_[1],$MIN);
143             }
144              
145             ### Solving the LP
146             sub solve {
147             my Math::LP $this = shift;
148             my $lag_solve = shift || 0; # lag_solve flag
149              
150             # 1. construct an equivalent lprec struct
151             if(defined $this->{lprec}) { # remove previously built lprec
152             &Math::LP::Solve::delete_lp($this->{lprec});
153             $this->{lprec} = undef;
154             }
155             my $lprec = $this->{lprec} = $this->make_lprec();
156            
157             # 2. solve the LP
158             $this->{solver_status} = $lag_solve
159             ? Math::LP::Solve::lag_solve($lprec)
160             : Math::LP::Solve::solve($lprec);
161              
162             # 3. copy the results to the appropriate Math::LP objects
163             $this->update_variable_values($lprec);
164             $this->update_slacks($lprec);
165             $this->update_dual_values($lprec);
166              
167             ### 4. delete the lprec struct
168             ##Math::LP::Solve::delete_lp($lprec);
169              
170             # 5. return true iff succeeded
171             return $this->{solver_status} == $OPTIMAL;
172             # I am not sure whether this is the wanted behaviour for $lag_solve == 1
173             }
174             sub make_coeff_array {
175             my Math::LP $this = shift;
176             my Math::LP::LinearCombination $lc = shift;
177              
178             # get a zero-initialized coefficient buffer
179             my $array = $this->get_dbuf($this->nr_cols() + 1, 0.0);
180             # +1 for the 0'th column, which does not represent a variable
181              
182             # fill out the coefficients
183             Math::LP::Solve::ptrset($array,$_->{coeff},$_->{var}->{col_index}) foreach values %{$lc->get_entries()};
184              
185             return $array;
186             }
187             sub make_lprec { # construct an lprec struct for the LP
188             my Math::LP $this = shift;
189             my $lprec = Math::LP::Solve::make_lp(0,$this->nr_cols()); # no constraints yet, correct nr. of variables
190            
191             # process all constraints
192             foreach my $constr (@{$this->{constraints}}) {
193             Math::LP::Solve::add_constraint($lprec,$this->make_coeff_array($constr->{lhs}),$constr->{type},$constr->{rhs});
194              
195             # Setting of the row name is disabled: it is not needed
196             #Math::LP::Solve::lprec_row_name_set($lprec,$constr->{index},$constr->{name})
197             # if defined $constr->{name};
198             }
199              
200             # process all variables
201             foreach my $var (values %{$this->{variables}}) {
202             &Math::LP::Solve::set_int($lprec,$var->{col_index},1) if $var->{is_int};
203             &Math::LP::Solve::set_upbo($lprec,$var->{col_index},$var->{upper_bound});
204             &Math::LP::Solve::set_lowbo($lprec,$var->{col_index},$var->{lower_bound});
205             # Setting of the col name is disabled: it is not needed and triggered a bug I still do not understand
206             #Math::LP::Solve::lprec_col_name_set($lprec,$var->{col_index},$var->{name});
207             }
208              
209             # set the objective function
210             if(defined($this->{objective_function})) {
211             Math::LP::Solve::set_obj_fn($lprec,$this->make_coeff_array($this->{objective_function}));
212             if ($this->{type} == $MAX) { Math::LP::Solve::set_maxim($lprec); }
213             elsif($this->{type} == $MIN) { Math::LP::Solve::set_minim($lprec); }
214             else {
215             $this->croak('No objective function type ($MAX or $MIN) set for solving');
216             }
217             }
218            
219             return $lprec;
220             }
221             sub update_variable_values { # copies the variable values to the variable objects
222             my Math::LP $this = shift;
223             my $lprec = shift;
224            
225             # the variable values are found in the solution vector
226             my $solution = Math::LP::Solve::lprec_best_solution_get($lprec);
227              
228             # The index offset is explained as follows
229             # + 1 because of the objective function value
230             # + nr_rows() because of the slacks
231             # - 1 because the 1st variable has index 1, not 0
232             my $offset = $this->nr_rows();
233              
234             # copy the appropriate value for each variable
235             foreach(values %{$this->{variables}}) {
236             my $var_index = $_->{col_index};
237             $_->{value} = Math::LP::Solve::ptrvalue($solution,$offset+$var_index);
238             }
239             }
240             sub update_slacks {
241             my Math::LP $this = shift;
242             my $lprec = shift;
243            
244             # the slacks are fetched from the solution vector
245             my $solution = Math::LP::Solve::lprec_best_solution_get($lprec);
246              
247             # copy the appropriate slack for each constraint
248             foreach(@{$this->{constraints}}) {
249             my $row_index = $_->{row_index};
250              
251             # The net offset used for fetching the row slack is calculated as follows:
252             # + 1 because of the objective function value
253             # - 1 because the 1st row has index 1, not 0
254             my $buggy_slack = Math::LP::Solve::ptrvalue($solution,$row_index);
255              
256             # Due to a bug (?), lp_solve does not return the slack for each
257             # constraint, but the evaluation of the lhs for the optimal variable
258             # values.
259             $_->{lhs}->{value} = $buggy_slack;
260              
261             # The real slack is easily derived from the lhs value.
262             $_->{slack} = $_->{rhs} - $buggy_slack;
263             }
264              
265             # Also fetch the objective function value
266             if(defined($this->{objective_function})) {
267             $this->{objective_function}->{value} = Math::LP::Solve::ptrvalue($solution,0);
268             }
269             }
270             sub update_dual_values {
271             my Math::LP $this = shift;
272             my $lprec = shift;
273              
274             # the dual values are fetched from the duals vector
275             my $duals = Math::LP::Solve::lprec_duals_get($lprec);
276              
277             # copy the appropriate dual value for each constraint
278             foreach(@{$this->{constraints}}) {
279             my $row_index = $_->{row_index};
280             $_->{dual_value} = Math::LP::Solve::ptrvalue($duals,$row_index)
281             }
282             }
283              
284             ### Queries
285             sub optimum {
286             my Math::LP $this = shift;
287             return undef if !defined($this->{objective_function});
288             return $this->{objective_function}->{value};
289             }
290              
291             sub get_constraints {
292             my Math::LP $this = shift;
293             wantarray ? @{$this->{constraints}} : $this->{constraints};
294             }
295              
296             sub get_variables {
297             my Math::LP $this = shift;
298             my $rh_v = $this->{variables};
299             my @vars = map { $rh_v->{$_} } sort keys %$rh_v;
300             wantarray ? @vars : \@vars;
301             }
302              
303             ### IO
304             sub stringify_lindo {
305             my Math::LP $this = shift;
306             my $str;
307              
308             # the objective function
309             my $type = $this->{type};
310             if($type == $MAX) {
311             $str = 'max ';
312             }
313             elsif($type == $MIN) {
314             $str = 'min ';
315             }
316             else {
317             die "Found LP with unrecognized type. Stopped";
318             }
319             $str .= $this->{objective_function}->stringify() . "\n";
320              
321             # the constraints
322             $str .= "subject to\n";
323             foreach(@{$this->{constraints}}) {
324             $str .= " " . $_->stringify() . "\n";
325             }
326             $str .= "end\n";
327              
328             # declaration of integer variables
329             foreach(grep { $_->{is_int} } @{$this->get_variables()}) {
330             $str .= "gin " . $_->{name} . "\n";
331             }
332              
333             return $str;
334             }
335              
336             sub stringify {
337             stringify_lindo(@_);
338             }
339              
340             sub stringify_solution { # only to be used when solving succeeded
341             my $this = shift;
342             my $str = "Objective function value = " . $this->optimum . "\n\n";
343             $str .= "VARIABLES:\n";
344             $str .= sprintf("%-12s = %12g\n", $_->{name}, $_->{value}) for @{$this->get_variables()};
345             #$str .= $_->{name} . '=' . $_->{value} . "\n" for @{$this->get_variables()};
346             $str .= sprintf("\n%32s %12s %12s\n", 'ROW', 'SLACK', 'DUAL PRICE');
347             #$str .= "\nROW\tSLACK\tDUAL PRICE\n";
348             $str .= sprintf("%32s %12g %12g\n", "[" . ($_->{name} || $_->{row_index}) . "]", $_->{slack}, $_->{dual_value})
349             for @{$this->get_constraints()};
350             #$str .= "[" . ($_->{name} || $_->{index}) . "]\t" . $_->{slack} . "\t" . $_->{dual_value} . "\n"
351             # for @{$this->get_constraints()};
352             return $str;
353             }
354              
355             sub print_solution {
356             my $this = shift;
357             print $this->stringify_solution();
358             }
359              
360             1;
361              
362             __END__