lp_select 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +165 -0
- data/README.md +96 -0
- data/Rakefile +11 -0
- data/lib/lp_select.rb +9 -0
- data/lib/lp_select/binaries/liblpsolve55.dylib +0 -0
- data/lib/lp_select/binaries/liblpsolve55.dylib-ppc +0 -0
- data/lib/lp_select/binaries/liblpsolve55.dylib.x86-64 +0 -0
- data/lib/lp_select/binaries/liblpsolve55.so +0 -0
- data/lib/lp_select/binaries/liblpsolve55.so-ux64 +0 -0
- data/lib/lp_select/binaries/lpsolve55.dll +0 -0
- data/lib/lp_select/lp_select.rb +239 -0
- data/lib/lp_select/lp_solve.rb +154 -0
- data/lib/lp_select/version.rb +4 -0
- data/lp_select.gemspec +25 -0
- data/test/lp_format_sample.txt +6 -0
- data/test/lp_select_test.rb +186 -0
- data/test/lp_solve_test.rb +251 -0
- data/test/test_helper.rb +9 -0
- metadata +121 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
2
|
+
Version 3, 29 June 2007
|
3
|
+
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
6
|
+
of this license document, but changing it is not allowed.
|
7
|
+
|
8
|
+
|
9
|
+
This version of the GNU Lesser General Public License incorporates
|
10
|
+
the terms and conditions of version 3 of the GNU General Public
|
11
|
+
License, supplemented by the additional permissions listed below.
|
12
|
+
|
13
|
+
0. Additional Definitions.
|
14
|
+
|
15
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
16
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
17
|
+
General Public License.
|
18
|
+
|
19
|
+
"The Library" refers to a covered work governed by this License,
|
20
|
+
other than an Application or a Combined Work as defined below.
|
21
|
+
|
22
|
+
An "Application" is any work that makes use of an interface provided
|
23
|
+
by the Library, but which is not otherwise based on the Library.
|
24
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
25
|
+
of using an interface provided by the Library.
|
26
|
+
|
27
|
+
A "Combined Work" is a work produced by combining or linking an
|
28
|
+
Application with the Library. The particular version of the Library
|
29
|
+
with which the Combined Work was made is also called the "Linked
|
30
|
+
Version".
|
31
|
+
|
32
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
33
|
+
Corresponding Source for the Combined Work, excluding any source code
|
34
|
+
for portions of the Combined Work that, considered in isolation, are
|
35
|
+
based on the Application, and not on the Linked Version.
|
36
|
+
|
37
|
+
The "Corresponding Application Code" for a Combined Work means the
|
38
|
+
object code and/or source code for the Application, including any data
|
39
|
+
and utility programs needed for reproducing the Combined Work from the
|
40
|
+
Application, but excluding the System Libraries of the Combined Work.
|
41
|
+
|
42
|
+
1. Exception to Section 3 of the GNU GPL.
|
43
|
+
|
44
|
+
You may convey a covered work under sections 3 and 4 of this License
|
45
|
+
without being bound by section 3 of the GNU GPL.
|
46
|
+
|
47
|
+
2. Conveying Modified Versions.
|
48
|
+
|
49
|
+
If you modify a copy of the Library, and, in your modifications, a
|
50
|
+
facility refers to a function or data to be supplied by an Application
|
51
|
+
that uses the facility (other than as an argument passed when the
|
52
|
+
facility is invoked), then you may convey a copy of the modified
|
53
|
+
version:
|
54
|
+
|
55
|
+
a) under this License, provided that you make a good faith effort to
|
56
|
+
ensure that, in the event an Application does not supply the
|
57
|
+
function or data, the facility still operates, and performs
|
58
|
+
whatever part of its purpose remains meaningful, or
|
59
|
+
|
60
|
+
b) under the GNU GPL, with none of the additional permissions of
|
61
|
+
this License applicable to that copy.
|
62
|
+
|
63
|
+
3. Object Code Incorporating Material from Library Header Files.
|
64
|
+
|
65
|
+
The object code form of an Application may incorporate material from
|
66
|
+
a header file that is part of the Library. You may convey such object
|
67
|
+
code under terms of your choice, provided that, if the incorporated
|
68
|
+
material is not limited to numerical parameters, data structure
|
69
|
+
layouts and accessors, or small macros, inline functions and templates
|
70
|
+
(ten or fewer lines in length), you do both of the following:
|
71
|
+
|
72
|
+
a) Give prominent notice with each copy of the object code that the
|
73
|
+
Library is used in it and that the Library and its use are
|
74
|
+
covered by this License.
|
75
|
+
|
76
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
77
|
+
document.
|
78
|
+
|
79
|
+
4. Combined Works.
|
80
|
+
|
81
|
+
You may convey a Combined Work under terms of your choice that,
|
82
|
+
taken together, effectively do not restrict modification of the
|
83
|
+
portions of the Library contained in the Combined Work and reverse
|
84
|
+
engineering for debugging such modifications, if you also do each of
|
85
|
+
the following:
|
86
|
+
|
87
|
+
a) Give prominent notice with each copy of the Combined Work that
|
88
|
+
the Library is used in it and that the Library and its use are
|
89
|
+
covered by this License.
|
90
|
+
|
91
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
92
|
+
document.
|
93
|
+
|
94
|
+
c) For a Combined Work that displays copyright notices during
|
95
|
+
execution, include the copyright notice for the Library among
|
96
|
+
these notices, as well as a reference directing the user to the
|
97
|
+
copies of the GNU GPL and this license document.
|
98
|
+
|
99
|
+
d) Do one of the following:
|
100
|
+
|
101
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
102
|
+
License, and the Corresponding Application Code in a form
|
103
|
+
suitable for, and under terms that permit, the user to
|
104
|
+
recombine or relink the Application with a modified version of
|
105
|
+
the Linked Version to produce a modified Combined Work, in the
|
106
|
+
manner specified by section 6 of the GNU GPL for conveying
|
107
|
+
Corresponding Source.
|
108
|
+
|
109
|
+
1) Use a suitable shared library mechanism for linking with the
|
110
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
111
|
+
a copy of the Library already present on the user's computer
|
112
|
+
system, and (b) will operate properly with a modified version
|
113
|
+
of the Library that is interface-compatible with the Linked
|
114
|
+
Version.
|
115
|
+
|
116
|
+
e) Provide Installation Information, but only if you would otherwise
|
117
|
+
be required to provide such information under section 6 of the
|
118
|
+
GNU GPL, and only to the extent that such information is
|
119
|
+
necessary to install and execute a modified version of the
|
120
|
+
Combined Work produced by recombining or relinking the
|
121
|
+
Application with a modified version of the Linked Version. (If
|
122
|
+
you use option 4d0, the Installation Information must accompany
|
123
|
+
the Minimal Corresponding Source and Corresponding Application
|
124
|
+
Code. If you use option 4d1, you must provide the Installation
|
125
|
+
Information in the manner specified by section 6 of the GNU GPL
|
126
|
+
for conveying Corresponding Source.)
|
127
|
+
|
128
|
+
5. Combined Libraries.
|
129
|
+
|
130
|
+
You may place library facilities that are a work based on the
|
131
|
+
Library side by side in a single library together with other library
|
132
|
+
facilities that are not Applications and are not covered by this
|
133
|
+
License, and convey such a combined library under terms of your
|
134
|
+
choice, if you do both of the following:
|
135
|
+
|
136
|
+
a) Accompany the combined library with a copy of the same work based
|
137
|
+
on the Library, uncombined with any other library facilities,
|
138
|
+
conveyed under the terms of this License.
|
139
|
+
|
140
|
+
b) Give prominent notice with the combined library that part of it
|
141
|
+
is a work based on the Library, and explaining where to find the
|
142
|
+
accompanying uncombined form of the same work.
|
143
|
+
|
144
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
145
|
+
|
146
|
+
The Free Software Foundation may publish revised and/or new versions
|
147
|
+
of the GNU Lesser General Public License from time to time. Such new
|
148
|
+
versions will be similar in spirit to the present version, but may
|
149
|
+
differ in detail to address new problems or concerns.
|
150
|
+
|
151
|
+
Each version is given a distinguishing version number. If the
|
152
|
+
Library as you received it specifies that a certain numbered version
|
153
|
+
of the GNU Lesser General Public License "or any later version"
|
154
|
+
applies to it, you have the option of following the terms and
|
155
|
+
conditions either of that published version or of any later version
|
156
|
+
published by the Free Software Foundation. If the Library as you
|
157
|
+
received it does not specify a version number of the GNU Lesser
|
158
|
+
General Public License, you may choose any version of the GNU Lesser
|
159
|
+
General Public License ever published by the Free Software Foundation.
|
160
|
+
|
161
|
+
If the Library as you received it specifies that a proxy can decide
|
162
|
+
whether future versions of the GNU Lesser General Public License shall
|
163
|
+
apply, that proxy's public statement of acceptance of any version is
|
164
|
+
permanent authorization for you to choose that version for the
|
165
|
+
Library.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# LpSelect
|
2
|
+
|
3
|
+
|
4
|
+
This gem provides both an FFI interface to the lp_solve library and a simplified interface
|
5
|
+
designed to pick from a set of choices to satisfy constraints (LPSelect).
|
6
|
+
|
7
|
+
lp_solve is a Mixed Integer Linear Programming (MILP) solver. It solves pure linear, (mixed) integer/binary,
|
8
|
+
semi-cont and special ordered sets (SOS) models. lp_solve is written in ANSI C and can be compiled on many
|
9
|
+
different platforms like Linux and WINDOWS.
|
10
|
+
|
11
|
+
The lp_solve library is included and dynamically loaded. The gem will look for the library in the
|
12
|
+
lib/binaries folder, and searches for the first library it can load in this order:
|
13
|
+
|
14
|
+
liblpsolve55.dylib
|
15
|
+
liblpsolve55.dylib-ppc
|
16
|
+
liblpsolve55.dylib.x86-64
|
17
|
+
liblpsolve55.so
|
18
|
+
liblpsolve55.so-ux64
|
19
|
+
lpsolve55.dll
|
20
|
+
|
21
|
+
|
22
|
+
You can download and compile the liblpsolve library from http://sourceforge.net/projects/lpsolve/
|
23
|
+
|
24
|
+
LPSelect is designed for selecting a set of things that satisfies as many constraints as possible.
|
25
|
+
The lp_select_test.rb test is a contrived example showing how it can be used to select a fruit salad
|
26
|
+
at the lowest cost that satisfies the most people. The price of each fruit is a weight and the objective
|
27
|
+
is to minimize the cost (ie weight) of the selected fruits. Each person's choices is added as a
|
28
|
+
constraint row.
|
29
|
+
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
Add this line to your application's Gemfile:
|
34
|
+
|
35
|
+
gem 'lp_select'
|
36
|
+
|
37
|
+
And then execute:
|
38
|
+
|
39
|
+
$ bundle
|
40
|
+
|
41
|
+
Or install it yourself as:
|
42
|
+
|
43
|
+
$ gem install lp_select
|
44
|
+
|
45
|
+
|
46
|
+
## Usage
|
47
|
+
|
48
|
+
Current usage is focused on a handful of known problems to be solved, but it is easily extensible.
|
49
|
+
|
50
|
+
The lp_solve library uses 1 indexed arrays.
|
51
|
+
|
52
|
+
There is more example usage in the LPSelect library and tests
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# Make a three row five column equation
|
56
|
+
@lp = LPSolve::make_lp(3, 5)
|
57
|
+
|
58
|
+
# Set some column names
|
59
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
60
|
+
LPSolve::set_col_name(@lp, 2, "bob")
|
61
|
+
# Add a constraint and a row name, the API expects a 1 indexed array
|
62
|
+
constraint_vars = [0, 0, 1]
|
63
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
64
|
+
p.write_array_of_double(constraint_vars)
|
65
|
+
LPSolve::add_constraint(@lp, p, LPSelect::EQ, 1.0.to_f)
|
66
|
+
end
|
67
|
+
LPSolve::set_row_name(@lp, 1, "onlyBob")
|
68
|
+
|
69
|
+
# Set the objective function and minimize it
|
70
|
+
constraint_vars = [1.0, 3.0]
|
71
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
72
|
+
p.write_array_of_double(constraint_vars)
|
73
|
+
LPSolve::set_obj_fn(@lp, p)
|
74
|
+
end
|
75
|
+
LPSolve::set_minim(@lp)
|
76
|
+
|
77
|
+
# Solve it and retreive the result
|
78
|
+
LPSolve::solve(@lp)
|
79
|
+
@objective = LPSolve::get_objective(@lp)
|
80
|
+
|
81
|
+
```
|
82
|
+
|
83
|
+
|
84
|
+
## Contributing
|
85
|
+
|
86
|
+
1. Fork it
|
87
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
88
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
89
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
90
|
+
5. Create new Pull Request
|
91
|
+
|
92
|
+
|
93
|
+
## Contributors
|
94
|
+
* James Prior
|
95
|
+
* Jake Sower
|
96
|
+
* Mattias Ekberg
|
data/Rakefile
ADDED
data/lib/lp_select.rb
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,239 @@
|
|
1
|
+
|
2
|
+
# -----------------------------------------------
|
3
|
+
#
|
4
|
+
# This class relies on the lpsolve library hooks, and
|
5
|
+
# serves as an interface for solving specialized
|
6
|
+
# selection problems. It uses binary variables
|
7
|
+
# with assigned names to determine what is selected
|
8
|
+
# -----------------------------------------------
|
9
|
+
|
10
|
+
include LPSolve
|
11
|
+
|
12
|
+
class LPSelect
|
13
|
+
|
14
|
+
attr_reader :vars, :lp, :model_name, :results, :objective, :constraints, :objective_row
|
15
|
+
CONSTRAINT_ROW = {:name => "", :vars => [], :op => nil, :target => nil} #our own little format
|
16
|
+
|
17
|
+
|
18
|
+
# Create a new selection equation, either by passing a list of variables
|
19
|
+
# corresponding to applicants, or by passing a YAML file or string that
|
20
|
+
# represents an equation serialized by this same model.
|
21
|
+
#
|
22
|
+
# Optionally it can load a file from disk in the lp format and solve it,
|
23
|
+
# although many of the utilities for inspecting the equation and serializing
|
24
|
+
# it will be non-functional
|
25
|
+
def initialize(opts = {})
|
26
|
+
|
27
|
+
# The objective is the overall equation to solve, in our case, the minimum
|
28
|
+
# sum of each applicants rank
|
29
|
+
@objective_row = {:direction => "min", :weights => {} }
|
30
|
+
@constraints = [] #This will be filled with dups of CONSTRAINT_ROW
|
31
|
+
@var_struct = nil
|
32
|
+
|
33
|
+
if opts[:filename] && File::exists?(opts[:filename])
|
34
|
+
load_from_file(opts[:filename])
|
35
|
+
elsif opts[:vars]
|
36
|
+
raise "Invalid option, vars must be an array" unless opts[:vars].is_a? Array
|
37
|
+
create_new(opts[:vars])
|
38
|
+
elsif opts[:yaml]
|
39
|
+
load_from_yaml(opts[:yaml])
|
40
|
+
else
|
41
|
+
raise "Must pass :filename, :yaml, or :vars"
|
42
|
+
end
|
43
|
+
|
44
|
+
@model_name = Time.now().strftime('%Y%m%d%H%M')
|
45
|
+
LPSolve::set_lp_name(@lp, @model_name)
|
46
|
+
|
47
|
+
LPSolve::set_verbose(@lp, LPSolve::SEVERE )
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_new(varnames)
|
51
|
+
self.vars = varnames
|
52
|
+
cols = @vars.length
|
53
|
+
@lp = LPSolve::make_lp(0, cols)
|
54
|
+
1.upto(cols) do |cnum|
|
55
|
+
cname = varnames[cnum-1] #For every column get the column name and index (NOT zero indexed)
|
56
|
+
LPSolve::set_binary(@lp, cnum, 1) #Define the column to be binary
|
57
|
+
LPSolve::set_col_name(@lp, cnum, cname.to_s.dup) #Set the name to what we passed
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_from_file(filename)
|
62
|
+
@lp = LPSolve::read_LP(filename, LPSolve::SEVERE, "")
|
63
|
+
|
64
|
+
loc_lp = LPSolve::copy_lp(@lp) #We make a copy to avoid advancing internal pointers.
|
65
|
+
existing_vars = []
|
66
|
+
1.upto(get_Ncolumns) do |col|
|
67
|
+
existing_vars << LPSolve::get_origcol_name(loc_lp, col).to_s
|
68
|
+
end
|
69
|
+
self.vars = existing_vars
|
70
|
+
end
|
71
|
+
|
72
|
+
# Weights should be a hash with variable names as keys, and
|
73
|
+
# individual multipliers as the values. Eg, { "v1" => 10 }
|
74
|
+
# would return a row like +10 v1
|
75
|
+
def set_objective(weights, direction = :min)
|
76
|
+
weights = weights.inject({}) do |options, (key, value)|
|
77
|
+
options[key.to_sym] = value
|
78
|
+
options
|
79
|
+
end
|
80
|
+
|
81
|
+
obj_fn = [1.0] # placeholder for zero indexed array
|
82
|
+
@vars.each do |var|
|
83
|
+
weight = weights[var] || 0.0
|
84
|
+
obj_fn << weight
|
85
|
+
end
|
86
|
+
|
87
|
+
FFI::MemoryPointer.new(:double, obj_fn.size) do |p|
|
88
|
+
p.write_array_of_double(obj_fn)
|
89
|
+
LPSolve::set_obj_fn(@lp, p)
|
90
|
+
end
|
91
|
+
|
92
|
+
if direction == :max
|
93
|
+
LPSolve::set_maxim(@lp)
|
94
|
+
else
|
95
|
+
LPSolve::set_minim(@lp)
|
96
|
+
end
|
97
|
+
|
98
|
+
@objective_row = {:direction => direction, :weights => weights }
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
def solve
|
103
|
+
|
104
|
+
#Important step.
|
105
|
+
solution = LPSolve::solve(@lp)
|
106
|
+
|
107
|
+
if solution == LPSolve::OPTIMAL || LPSolve::SUBOPTIMAL
|
108
|
+
#--------------------
|
109
|
+
# Get the values
|
110
|
+
#--------------------
|
111
|
+
@results = {}
|
112
|
+
|
113
|
+
retvals = []
|
114
|
+
FFI::MemoryPointer.new(:double, @vars.size) do |p|
|
115
|
+
if LPSolve::get_variables(@lp, p)
|
116
|
+
retvals = p.get_array_of_double(0, @vars.size)
|
117
|
+
@vars.each_with_index do |c, idx|
|
118
|
+
@results[c] = retvals[idx]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
#--------------------
|
124
|
+
# Set the objective (eg, final sum)
|
125
|
+
#--------------------
|
126
|
+
@objective = LPSolve::get_objective(@lp)
|
127
|
+
end
|
128
|
+
return solution
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
# This piece is what's used to ensure that the function selects the right amounts from
|
134
|
+
# the right categories. For example, to select at least one applicant from Maine,
|
135
|
+
# we could add a constraint saying that the sum of all applicant variables
|
136
|
+
# representing the applicants from Maine must be greater than or equal to 1.
|
137
|
+
def add_constraint(rowdef = CONSTRAINT_ROW)
|
138
|
+
|
139
|
+
raise "You must specify at least one variable to add a constraint" if rowdef[:vars].length == 0
|
140
|
+
raise "You must specify an operation" if rowdef[:op].nil?
|
141
|
+
raise "You must specify a target (ie, right hand side)" if rowdef[:target].nil?
|
142
|
+
raise "Target must be a number" unless rowdef[:target].is_a?(Float) || rowdef[:target].is_a?(Integer)
|
143
|
+
|
144
|
+
varnames = rowdef[:vars].map! {|v| v.to_sym}
|
145
|
+
|
146
|
+
#The API expects a 1 indexed array, so start with an empty item in row_constraints[0]
|
147
|
+
row_constraints = [0.0]
|
148
|
+
@vars.each do |v|
|
149
|
+
# Since we're only interested in binary columns, putting in a 1 or a zero is sufficient
|
150
|
+
# to either add them to the constraint or not
|
151
|
+
row_constraints << (varnames.include?(v) ? 1.0 : 0.0)
|
152
|
+
end
|
153
|
+
|
154
|
+
FFI::MemoryPointer.new(:double, row_constraints.size) do |p|
|
155
|
+
p.write_array_of_double(row_constraints)
|
156
|
+
LPSolve::add_constraint(@lp, p, rowdef[:op], rowdef[:target].to_f)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Every row has a name, and it's helpful if it suggests something about the constraint,
|
160
|
+
# eg maine or minorities
|
161
|
+
if rowdef[:name] == ""
|
162
|
+
rowdef[:name] = "R#{@constraints.length+1}"
|
163
|
+
end
|
164
|
+
@constraints << rowdef
|
165
|
+
LPSolve::set_row_name(@lp, @constraints.length, rowdef[:name])
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_file(filename)
|
169
|
+
loc_lp = LPSolve::copy_lp(@lp)
|
170
|
+
valid = LPSolve::write_lp(loc_lp, filename)
|
171
|
+
raise "Could not write to #{filename}" unless valid
|
172
|
+
end
|
173
|
+
|
174
|
+
def to_yaml
|
175
|
+
@complete = {}
|
176
|
+
@complete["vars"] = @vars
|
177
|
+
@complete["model_name"] = @model_name
|
178
|
+
@complete["constraints"] = @constraints
|
179
|
+
@complete["objective"] = @objective_row unless @objective_row[:weights].empty?
|
180
|
+
@complete.to_yaml
|
181
|
+
end
|
182
|
+
|
183
|
+
def load_from_yaml(yaml)
|
184
|
+
@complete = YAML::load(yaml)
|
185
|
+
raise "could not load from yaml" unless @complete
|
186
|
+
create_new(@complete["vars"])
|
187
|
+
LPSolve::set_lp_name(@lp, @complete["model_name"])
|
188
|
+
@complete["constraints"].each do |c|
|
189
|
+
add_constraint(c)
|
190
|
+
end
|
191
|
+
|
192
|
+
if @complete['objective']
|
193
|
+
set_objective(@complete['objective'][:weights], @complete['objective'][:direction])
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def to_lp_format
|
198
|
+
lp_text = "/* #{@model_name} */\n\n"
|
199
|
+
|
200
|
+
lp_text << "/* Objective function */\n"
|
201
|
+
lp_text << "#{@objective_row[:direction]}:"
|
202
|
+
weights = @objective_row[:weights]
|
203
|
+
weights.keys.sort_by{|s| s.to_s}.map{|key| [key, weights[key]] }.each do |var,constant|
|
204
|
+
sign = constant > 0 ? "+" : "-"
|
205
|
+
if constant.abs == 1
|
206
|
+
lp_text << " #{sign}#{var}"
|
207
|
+
else
|
208
|
+
lp_text << " #{sign}#{constant} #{var}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
lp_text << ";\n\n"
|
212
|
+
|
213
|
+
lp_text << "/* Constraints */\n"
|
214
|
+
@constraints.each do |row|
|
215
|
+
row_vars = row[:vars].map{|x| "+#{x}"}.sort.join(" ")
|
216
|
+
row_op = LPSolve.op_to_s(row[:op])
|
217
|
+
lp_text << "#{row[:name]}: #{row_vars} #{row_op} #{row[:target]}; \n\n"
|
218
|
+
end
|
219
|
+
|
220
|
+
lp_text << "\n/* Varible Bounds */\n"
|
221
|
+
@vars.each do |v|
|
222
|
+
lp_text << "#{v} <= 1;\n"
|
223
|
+
end
|
224
|
+
|
225
|
+
lp_text << "\n/* Variable Definitions */\n"
|
226
|
+
lp_text << "int #{@vars.join(", ")};"
|
227
|
+
return lp_text
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def vars=(new_vars)
|
233
|
+
@vars = new_vars.collect(&:to_sym)
|
234
|
+
end
|
235
|
+
|
236
|
+
def get_Ncolumns
|
237
|
+
@n_columns ||= LPSolve::get_Ncolumns(@lp)
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# -----------------------------------------------
|
2
|
+
#
|
3
|
+
# This module uses Ruby's DL library to dynamically load
|
4
|
+
# the liblpsolve library, which is a linear equation solver.
|
5
|
+
#
|
6
|
+
# The sourceforge summary says:
|
7
|
+
# Mixed Integer Linear Programming (MILP) solver lp_solve solves pure linear,
|
8
|
+
# (mixed) integer/binary, semi-cont and special ordered sets (SOS) models.
|
9
|
+
# lp_solve is written in ANSI C and can be compiled on many different platforms
|
10
|
+
# like Linux and WINDOWS
|
11
|
+
#
|
12
|
+
# Read more at http://sourceforge.net/projects/lpsolve/
|
13
|
+
#
|
14
|
+
# This module mostly serves to load the file and attach to the
|
15
|
+
# API functions
|
16
|
+
#
|
17
|
+
# -----------------------------------------------
|
18
|
+
|
19
|
+
module LPSolve
|
20
|
+
extend FFI::Library
|
21
|
+
base = File.expand_path(File.join( File.dirname(__FILE__), "binaries") )
|
22
|
+
err = nil
|
23
|
+
["liblpsolve55.so", "liblpsolve55.so-ux64", "liblpsolve55.dylib", "liblpsolve55.dylib.x86-64", "liblpsolve55.dylib-ppc", "lpsolve55.dll"].each do |lib|
|
24
|
+
begin
|
25
|
+
err = nil
|
26
|
+
ffi_lib File.join(base, lib)
|
27
|
+
break
|
28
|
+
rescue LoadError => e
|
29
|
+
err = e
|
30
|
+
end
|
31
|
+
end
|
32
|
+
raise "Could not find suitable liblpsolve55 library #{err}" unless err.nil?
|
33
|
+
|
34
|
+
#Constants used when assigning constraints
|
35
|
+
LE = 1 # <=
|
36
|
+
EQ = 3 # ==
|
37
|
+
GE = 2 # >=
|
38
|
+
|
39
|
+
def op_to_s(op_int)
|
40
|
+
case op_int
|
41
|
+
when LPSelect::GE then ">="
|
42
|
+
when LPSelect::LE then "<="
|
43
|
+
when LPSelect::EQ then "=="
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#Constants used for verbosity
|
48
|
+
CRITICAL = 1
|
49
|
+
SEVERE = 2
|
50
|
+
IMPORTANT = 3
|
51
|
+
NORMAL = 4
|
52
|
+
DETAILED = 5
|
53
|
+
FULL = 6
|
54
|
+
|
55
|
+
#Constants used for solve results
|
56
|
+
NOMEMORY = -2
|
57
|
+
OPTIMAL = 0
|
58
|
+
SUBOPTIMAL = 1
|
59
|
+
INFEASIBLE = 2
|
60
|
+
UNBOUNDED = 3
|
61
|
+
DEGENERATE = 4
|
62
|
+
NUMFAILURE = 5
|
63
|
+
USERABORT = 6
|
64
|
+
TIMEOUT = 7
|
65
|
+
PRESOLVED = 9
|
66
|
+
PROCFAIL = 10
|
67
|
+
PROCBREAK = 11
|
68
|
+
FEASFOUND = 12
|
69
|
+
NOFEASFOUND = 13
|
70
|
+
|
71
|
+
typedef :double, :REAL
|
72
|
+
typedef :long, :lprec
|
73
|
+
|
74
|
+
|
75
|
+
# All of the function signatures here come from
|
76
|
+
# http://lpsolve.sourceforge.net/5.5/lp_solveAPIreference.htm
|
77
|
+
|
78
|
+
# void lp_solve_version(int *majorversion, int *minorversion, int *release, int *build)
|
79
|
+
attach_function :lp_solve_version, [:pointer, :pointer, :pointer, :pointer], :void
|
80
|
+
|
81
|
+
# lprec *read_LP(char *filename, int verbose, char *lp_name)
|
82
|
+
attach_function :read_LP, [:string, :int, :string], :pointer
|
83
|
+
|
84
|
+
# int solve(lprec *lp)
|
85
|
+
attach_function :solve, [:pointer], :int
|
86
|
+
|
87
|
+
# unsigned char get_variables(lprec *lp, REAL *var);
|
88
|
+
attach_function :get_variables, [:pointer, :pointer], :char
|
89
|
+
|
90
|
+
# int get_Ncolumns(lprec *lp);
|
91
|
+
attach_function :get_Ncolumns, [:pointer], :int
|
92
|
+
|
93
|
+
# char *get_col_name(lprec *lp, int column);
|
94
|
+
attach_function :get_col_name, [:pointer, :int], :string
|
95
|
+
|
96
|
+
# char *get_origcol_name(lprec *lp, int column);
|
97
|
+
attach_function :get_origcol_name, [:pointer, :int], :string
|
98
|
+
|
99
|
+
# lprec *copy_lp(lprec *lp);
|
100
|
+
attach_function :copy_lp, [:pointer], :pointer
|
101
|
+
|
102
|
+
# void print_lp(lprec *lp);
|
103
|
+
attach_function :print_lp, [:pointer], :void
|
104
|
+
|
105
|
+
# lprec *make_lp(int rows, int columns);
|
106
|
+
attach_function :make_lp, [:int, :int], :pointer
|
107
|
+
|
108
|
+
# void delete_lp(lprec *lp);
|
109
|
+
attach_function :delete_lp, [:pointer], :void
|
110
|
+
|
111
|
+
# unsigned char set_binary(lprec *lp, int column, unsigned char must_be_bin);
|
112
|
+
attach_function :set_binary, [:pointer, :int, :char], :char
|
113
|
+
|
114
|
+
# unsigned char set_col_name(lprec *lp, int column, char *new_name);
|
115
|
+
attach_function :set_col_name, [:pointer, :int, :string], :char
|
116
|
+
|
117
|
+
# unsigned char set_obj_fn(lprec *lp, REAL *row);
|
118
|
+
attach_function :set_obj_fn, [:pointer, :pointer], :char
|
119
|
+
|
120
|
+
# unsigned char add_constraint(lprec *lp, REAL *row, int constr_type, REAL rh);
|
121
|
+
attach_function :add_constraint, [:pointer, :pointer, :int, :REAL], :char
|
122
|
+
|
123
|
+
# unsigned char set_row_name(lprec *lp, int row, char *new_name);
|
124
|
+
attach_function :set_row_name, [:pointer, :int, :string], :char
|
125
|
+
|
126
|
+
# REAL get_objective(lprec *lp);
|
127
|
+
attach_function :get_objective, [:pointer], :REAL
|
128
|
+
|
129
|
+
# void set_verbose(lprec *lp, int verbose);
|
130
|
+
attach_function :set_verbose, [:pointer, :int], :void
|
131
|
+
|
132
|
+
# unsigned char set_lp_name(lprec *lp, char *lpname);
|
133
|
+
attach_function :set_lp_name, [:pointer, :string], :char
|
134
|
+
|
135
|
+
# unsigned char write_lp(lprec *lp, char *filename);
|
136
|
+
attach_function :write_lp, [:pointer, :string], :char
|
137
|
+
|
138
|
+
# void set_maxim(lprec *lp);
|
139
|
+
attach_function :set_maxim, [:pointer], :void
|
140
|
+
|
141
|
+
# void set_minim(lprec *lp);
|
142
|
+
attach_function :set_minim, [:pointer], :void
|
143
|
+
|
144
|
+
def self.version
|
145
|
+
maj_ptr = FFI::MemoryPointer.new(:pointer, 1)
|
146
|
+
min_ptr = FFI::MemoryPointer.new(:pointer, 1)
|
147
|
+
rel_ptr = FFI::MemoryPointer.new(:pointer, 1)
|
148
|
+
bld_ptr = FFI::MemoryPointer.new(:pointer, 1)
|
149
|
+
LPSolve::lp_solve_version(maj_ptr, min_ptr, rel_ptr, bld_ptr)
|
150
|
+
|
151
|
+
"#{maj_ptr.read_int}.#{min_ptr.read_int}.#{rel_ptr.read_int} build #{bld_ptr.read_int}"
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
data/lp_select.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lp_select/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "lp_select"
|
8
|
+
spec.version = LpSelect::VERSION
|
9
|
+
spec.authors = ["Jake Sower", "James Prior"]
|
10
|
+
spec.email = ["j.sower@asee.org", "j.prior@asee.org"]
|
11
|
+
spec.description = %q{Ruby bindings for LPSolve}
|
12
|
+
spec.summary = %q{Ruby bindings for LPSolve}
|
13
|
+
spec.homepage = "https://github.com/asee/lp_select"
|
14
|
+
spec.license = "LGPL"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "ffi"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class LpSelectTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@fruits = {
|
6
|
+
"apple" => 1,
|
7
|
+
"bananna" => 0.3,
|
8
|
+
"pear" => 1.4,
|
9
|
+
"strawberry" => 3,
|
10
|
+
"pineapple" => 5,
|
11
|
+
"watermelon" => 4.3,
|
12
|
+
"grapes" => 2.4,
|
13
|
+
"orange" => 0.89,
|
14
|
+
"kiwi" => 3.49,
|
15
|
+
"mango" => 4.45,
|
16
|
+
"cherry" => 6,
|
17
|
+
"blueberry" => 5.50
|
18
|
+
}
|
19
|
+
|
20
|
+
@lprec = LPSelect.new(:vars => @fruits.keys)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_set_objective
|
24
|
+
objective_row = {}
|
25
|
+
assert_nothing_raised do
|
26
|
+
@lprec.set_objective(@fruits, :min) # min is the default
|
27
|
+
objective_row = @lprec.objective_row
|
28
|
+
end
|
29
|
+
assert_equal :min, objective_row[:direction]
|
30
|
+
assert_equal @fruits.length, objective_row[:weights].length
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_add_constraints
|
34
|
+
# Alice likes berries, she gets at least one
|
35
|
+
@lprec.add_constraint({:name => "alice_picks", :vars => ["blueberry", "strawberry"], :op => LPSelect::GE, :target => 1})
|
36
|
+
assert_not_nil @lprec.constraints.detect{|x| x[:name] == 'alice_picks'}
|
37
|
+
|
38
|
+
# Bob likes tropical fruit, but he shouldn't get any more than two
|
39
|
+
@lprec.add_constraint({:name => "bob_picks", :vars => ["kiwi", "mango", "pineapple"], :op => LPSelect::LE, :target => 2})
|
40
|
+
assert_not_nil @lprec.constraints.detect{|x| x[:name] == 'bob_picks'}
|
41
|
+
|
42
|
+
# Carol is a toddler and gets to pick only two things.
|
43
|
+
@lprec.add_constraint({:name => "carol_picks", :vars => ["apple", "bananna", "grapes"], :op => LPSelect::EQ, :target => 2})
|
44
|
+
assert_not_nil @lprec.constraints.detect{|x| x[:name] == 'carol_picks'}
|
45
|
+
|
46
|
+
# Dan is an omnivore and we like him enough that he should get at least three.
|
47
|
+
@lprec.add_constraint({:name => "don_picks", :vars => @fruits.keys, :op => LPSelect::GE, :target => 3})
|
48
|
+
assert_not_nil @lprec.constraints.detect{|x| x[:name] == 'don_picks'}
|
49
|
+
|
50
|
+
assert_equal 4, @lprec.constraints.count
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_yaml_serialize
|
54
|
+
@lprec.set_objective(@fruits, :min) # min is the default
|
55
|
+
|
56
|
+
# Alice likes berries, she gets at least one
|
57
|
+
@lprec.add_constraint({:name => "alice_picks", :vars => ["blueberry", "strawberry"], :op => LPSelect::GE, :target => 1})
|
58
|
+
|
59
|
+
# Bob likes tropical fruit, but he shouldn't get any more than two
|
60
|
+
@lprec.add_constraint({:name => "bob_picks", :vars => ["kiwi", "mango", "pineapple"], :op => LPSelect::LE, :target => 2})
|
61
|
+
|
62
|
+
# Carol is a toddler and gets to pick only two things.
|
63
|
+
@lprec.add_constraint({:name => "carol_picks", :vars => ["apple", "bananna", "grapes"], :op => LPSelect::EQ, :target => 2})
|
64
|
+
|
65
|
+
# Dan is an omnivore and we like him enough that he should get at least three.
|
66
|
+
@lprec.add_constraint({:name => "don_picks", :vars => @fruits.keys, :op => LPSelect::GE, :target => 3})
|
67
|
+
|
68
|
+
# etc...
|
69
|
+
|
70
|
+
yaml_source = nil
|
71
|
+
assert_nothing_raised do
|
72
|
+
yaml_source = @lprec.to_yaml
|
73
|
+
end
|
74
|
+
assert_not_nil yaml_source
|
75
|
+
|
76
|
+
alt = LPSelect.new(:yaml => yaml_source)
|
77
|
+
assert_equal alt.objective_row, @lprec.objective_row
|
78
|
+
assert_equal alt.vars, @lprec.vars
|
79
|
+
assert_equal alt.constraints, @lprec.constraints
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_lp_format_serialize
|
83
|
+
@lprec.set_objective(@fruits, :min) # min is the default
|
84
|
+
|
85
|
+
# Alice likes berries, she gets at least one
|
86
|
+
@lprec.add_constraint({:name => "alice_picks", :vars => ["blueberry", "strawberry"], :op => LPSelect::GE, :target => 1})
|
87
|
+
|
88
|
+
# Bob likes tropical fruit, but he shouldn't get any more than two
|
89
|
+
@lprec.add_constraint({:name => "bob_picks", :vars => ["kiwi", "mango", "pineapple"], :op => LPSelect::LE, :target => 2})
|
90
|
+
|
91
|
+
# Carol is a toddler and gets to pick only two things.
|
92
|
+
@lprec.add_constraint({:name => "carol_picks", :vars => ["apple", "bananna", "grapes"], :op => LPSelect::EQ, :target => 2})
|
93
|
+
|
94
|
+
# Dan is an omnivore and we like him enough that he should get at least three.
|
95
|
+
@lprec.add_constraint({:name => "don_picks", :vars => @fruits.keys, :op => LPSelect::GE, :target => 3})
|
96
|
+
|
97
|
+
# etc...
|
98
|
+
begin
|
99
|
+
destination = Tempfile.new("lp_select_test")
|
100
|
+
lp_source = nil
|
101
|
+
assert_nothing_raised do
|
102
|
+
lp_source = @lprec.to_lp_format
|
103
|
+
@lprec.to_file(destination.path)
|
104
|
+
end
|
105
|
+
|
106
|
+
assert_not_nil lp_source
|
107
|
+
|
108
|
+
@lprec.constraints.each do |c|
|
109
|
+
assert lp_source.include?(c[:name]), "Expected the source to include #{c[:name]}"
|
110
|
+
end
|
111
|
+
|
112
|
+
@fruits.keys.each do |name|
|
113
|
+
assert lp_source.include?(name), "Expected the source to include #{name}"
|
114
|
+
end
|
115
|
+
|
116
|
+
assert File.size(destination.path) > 0
|
117
|
+
|
118
|
+
alt = LPSelect.new(:filename => destination.path)
|
119
|
+
assert_equal alt.vars, @lprec.vars
|
120
|
+
|
121
|
+
ensure
|
122
|
+
destination.unlink
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_should_solve_with_min_price
|
127
|
+
@lprec.set_objective(@fruits, :min) # min is the default
|
128
|
+
|
129
|
+
# Alice likes berries, she gets at least one, strawberry is expected since it is the cheapest
|
130
|
+
@lprec.add_constraint({:name => "alice_picks", :vars => ["blueberry", "strawberry"], :op => LPSelect::GE, :target => 1})
|
131
|
+
|
132
|
+
# Bob likes tropical fruit, but he shouldn't get any more than two. Since we're minimizing we expect none of these
|
133
|
+
@lprec.add_constraint({:name => "bob_picks", :vars => ["kiwi", "mango", "pineapple"], :op => LPSelect::LE, :target => 2})
|
134
|
+
|
135
|
+
# Carol is a toddler and gets to pick only two things, the two cheapest
|
136
|
+
@lprec.add_constraint({:name => "carol_picks", :vars => ["apple", "bananna", "grapes"], :op => LPSelect::EQ, :target => 2})
|
137
|
+
|
138
|
+
# Dan is an omnivore and we like him enough that he should get at least three, so three total picks
|
139
|
+
@lprec.add_constraint({:name => "don_picks", :vars => @fruits.keys, :op => LPSelect::GE, :target => 3})
|
140
|
+
|
141
|
+
status = nil
|
142
|
+
assert_nothing_raised do
|
143
|
+
status = @lprec.solve
|
144
|
+
end
|
145
|
+
|
146
|
+
assert_not_nil status
|
147
|
+
assert_equal LPSolve::OPTIMAL, status
|
148
|
+
|
149
|
+
assert_not_nil @lprec.results
|
150
|
+
selected_fruits = @lprec.results.collect{|k,v| v == 1.0 ? k : nil}.flatten.compact
|
151
|
+
assert_equal 4.3, @lprec.objective
|
152
|
+
assert_equal [:apple, :bananna, :strawberry], selected_fruits
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_should_solve_with_max_price
|
156
|
+
@lprec.set_objective(@fruits, :max)
|
157
|
+
|
158
|
+
# Alice likes berries, she gets at least one. Both should show up since we are maximizing
|
159
|
+
@lprec.add_constraint({:name => "alice_picks", :vars => ["blueberry", "strawberry"], :op => LPSelect::GE, :target => 1})
|
160
|
+
|
161
|
+
# Bob likes tropical fruit, but he shouldn't get any more than two - mango and pineapple since they are the most expensive
|
162
|
+
@lprec.add_constraint({:name => "bob_picks", :vars => ["kiwi", "mango", "pineapple"], :op => LPSelect::LE, :target => 2})
|
163
|
+
|
164
|
+
# Carol is a toddler and gets to pick only two things. Apple and grapes since they are the most expensive, but no bananas
|
165
|
+
@lprec.add_constraint({:name => "carol_picks", :vars => ["apple", "bananna", "grapes"], :op => LPSelect::EQ, :target => 2})
|
166
|
+
|
167
|
+
# Dan is an omnivore and we like him enough that he should get at least three. - so everythign else here
|
168
|
+
@lprec.add_constraint({:name => "don_picks", :vars => @fruits.keys, :op => LPSelect::GE, :target => 3})
|
169
|
+
|
170
|
+
expected_fruits = @fruits.keys.collect(&:to_sym) - [:bananna, :kiwi]
|
171
|
+
|
172
|
+
status = nil
|
173
|
+
assert_nothing_raised do
|
174
|
+
status = @lprec.solve
|
175
|
+
end
|
176
|
+
|
177
|
+
assert_not_nil status
|
178
|
+
assert_equal LPSolve::OPTIMAL, status
|
179
|
+
|
180
|
+
assert_not_nil @lprec.results
|
181
|
+
selected_fruits = @lprec.results.collect{|k,v| v == 1.0 ? k : nil}.flatten.compact
|
182
|
+
assert_equal 33.94, @lprec.objective
|
183
|
+
assert_equal expected_fruits.collect(&:to_s).sort, selected_fruits.collect(&:to_s).sort
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class LpSolveTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# void lp_solve_version(int *majorversion, int *minorversion, int *release, int *build)
|
6
|
+
def test_lp_solve_version
|
7
|
+
assert LPSolve.version.include?("5.5.0 build ")
|
8
|
+
end
|
9
|
+
|
10
|
+
# lprec *make_lp(int rows, int columns);
|
11
|
+
def test_make_lp
|
12
|
+
assert_nothing_raised do
|
13
|
+
@lp = LPSolve::make_lp(0, 0)
|
14
|
+
end
|
15
|
+
assert_not_nil @lp
|
16
|
+
end
|
17
|
+
|
18
|
+
# unsigned char set_binary(lprec *lp, int column, unsigned char must_be_bin);
|
19
|
+
def test_set_binary
|
20
|
+
@lp = LPSolve::make_lp(0, 1)
|
21
|
+
assert_nothing_raised do
|
22
|
+
LPSolve::set_binary(@lp, 1, 1) #Define the column to be binary
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# unsigned char set_col_name(lprec *lp, int column, char *new_name);
|
27
|
+
def test_set_col_name
|
28
|
+
@lp = LPSolve::make_lp(0, 1)
|
29
|
+
assert_nothing_raised do
|
30
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# unsigned char set_lp_name(lprec *lp, char *lpname);
|
35
|
+
def test_set_lp_name
|
36
|
+
@lp = LPSolve::make_lp(0, 0)
|
37
|
+
assert_nothing_raised do
|
38
|
+
LPSolve::set_lp_name(@lp, "Hi mom")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# void set_verbose(lprec *lp, int verbose);
|
43
|
+
def test_set_verbose
|
44
|
+
@lp = LPSolve::make_lp(0, 0)
|
45
|
+
assert_nothing_raised do
|
46
|
+
LPSolve::set_verbose(@lp, LPSolve::SEVERE )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# lprec *copy_lp(lprec *lp);
|
51
|
+
def test_copy_lp
|
52
|
+
@lp = LPSolve::make_lp(0, 0)
|
53
|
+
assert_nothing_raised do
|
54
|
+
LPSolve::copy_lp(@lp)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# lprec *read_LP(char *filename, int verbose, char *lp_name)
|
59
|
+
def test_read_lp
|
60
|
+
filename = File.expand_path("../lp_format_sample.txt", __FILE__)
|
61
|
+
assert_nothing_raised do
|
62
|
+
@lp = LPSolve::read_LP(filename, LPSolve::SEVERE, "")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# char *get_origcol_name(lprec *lp, int column);
|
67
|
+
def test_get_origcol_name
|
68
|
+
@lp = LPSolve::make_lp(0, 1)
|
69
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
70
|
+
|
71
|
+
colname = LPSolve::get_origcol_name(@lp, 1)
|
72
|
+
assert_equal "fred", colname.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
# void set_maxim(lprec *lp);
|
76
|
+
def test_set_maxim
|
77
|
+
@lp = LPSolve::make_lp(0, 0)
|
78
|
+
assert_nothing_raised do
|
79
|
+
LPSolve::set_maxim(@lp)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# void set_minim(lprec *lp);
|
84
|
+
def test_set_minim
|
85
|
+
@lp = LPSolve::make_lp(0, 0)
|
86
|
+
assert_nothing_raised do
|
87
|
+
LPSolve::set_minim(@lp)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# unsigned char write_lp(lprec *lp, char *filename);
|
92
|
+
def test_write_lp
|
93
|
+
destination = Tempfile.new("lp_solve_test")
|
94
|
+
begin
|
95
|
+
@lp = LPSolve::make_lp(0, 1)
|
96
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
97
|
+
|
98
|
+
assert_nothing_raised do
|
99
|
+
LPSolve::write_lp(@lp, destination.path)
|
100
|
+
end
|
101
|
+
assert File.size(destination.path) > 0
|
102
|
+
ensure
|
103
|
+
destination.unlink
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# int get_Ncolumns(lprec *lp);
|
108
|
+
def test_get_Ncolumns
|
109
|
+
@lp = LPSolve::make_lp(0, 3)
|
110
|
+
ncols = LPSolve::get_Ncolumns(@lp)
|
111
|
+
assert_equal 3, ncols
|
112
|
+
end
|
113
|
+
|
114
|
+
# char *get_col_name(lprec *lp, int column);
|
115
|
+
def test_get_col_name
|
116
|
+
@lp = LPSolve::make_lp(0, 1)
|
117
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
118
|
+
|
119
|
+
colname = LPSolve::get_origcol_name(@lp, 1)
|
120
|
+
assert_equal "fred", colname.to_s
|
121
|
+
end
|
122
|
+
|
123
|
+
# unsigned char add_constraint(lprec *lp, REAL *row, int constr_type, REAL rh);
|
124
|
+
def test_add_constraint
|
125
|
+
@lp = LPSolve::make_lp(0, 2)
|
126
|
+
|
127
|
+
#The API expects a 1 indexed array
|
128
|
+
constraint_vars = [0, 0, 1]
|
129
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
130
|
+
p.write_array_of_double(constraint_vars)
|
131
|
+
assert_nothing_raised do
|
132
|
+
LPSolve::add_constraint(@lp, p, LPSelect::EQ, 1.0.to_f)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# unsigned char set_row_name(lprec *lp, int row, char *new_name);
|
138
|
+
def test_set_row_name
|
139
|
+
@lp = LPSolve::make_lp(0, 2)
|
140
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
141
|
+
LPSolve::set_col_name(@lp, 2, "bob")
|
142
|
+
|
143
|
+
#The API expects a 1 indexed array
|
144
|
+
constraint_vars = [0, 0, 1]
|
145
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
146
|
+
p.write_array_of_double(constraint_vars)
|
147
|
+
LPSolve::add_constraint(@lp, p, LPSelect::EQ, 1.0.to_f)
|
148
|
+
end
|
149
|
+
|
150
|
+
assert_nothing_raised do
|
151
|
+
LPSolve::set_row_name(@lp, 1, "onlyBob")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# unsigned char set_obj_fn(lprec *lp, REAL *row);
|
156
|
+
def test_set_obj_fn
|
157
|
+
@lp = LPSolve::make_lp(0, 1)
|
158
|
+
|
159
|
+
#The API expects a 1 indexed array
|
160
|
+
constraint_vars = [1.0, 3.0]
|
161
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
162
|
+
p.write_array_of_double(constraint_vars)
|
163
|
+
assert_nothing_raised do
|
164
|
+
LPSolve::set_obj_fn(@lp, p)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# void delete_lp(lprec *lp);
|
171
|
+
def test_delete_lp
|
172
|
+
@lp = LPSolve::make_lp(0, 1)
|
173
|
+
assert_nothing_raised do
|
174
|
+
LPSolve::delete_lp(@lp)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# int solve(lprec *lp)
|
179
|
+
def test_solve
|
180
|
+
@lp = LPSolve::make_lp(0, 2)
|
181
|
+
LPSolve::set_verbose(@lp, LPSolve::SEVERE )
|
182
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
183
|
+
LPSolve::set_col_name(@lp, 2, "bob")
|
184
|
+
|
185
|
+
#The API expects a 1 indexed array
|
186
|
+
constraint_vars = [0, 0, 1]
|
187
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
188
|
+
p.write_array_of_double(constraint_vars)
|
189
|
+
LPSolve::add_constraint(@lp, p, LPSelect::EQ, 1.0.to_f)
|
190
|
+
end
|
191
|
+
|
192
|
+
LPSolve::set_minim(@lp)
|
193
|
+
|
194
|
+
assert_nothing_raised do
|
195
|
+
solution = LPSolve::solve(@lp)
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
# REAL get_objective(lprec *lp);
|
201
|
+
def test_get_objective
|
202
|
+
@lp = LPSolve::make_lp(0, 2)
|
203
|
+
LPSolve::set_verbose(@lp, LPSolve::SEVERE )
|
204
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
205
|
+
LPSolve::set_col_name(@lp, 2, "bob")
|
206
|
+
|
207
|
+
#The API expects a 1 indexed array
|
208
|
+
constraint_vars = [0, 0, 1]
|
209
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
210
|
+
p.write_array_of_double(constraint_vars)
|
211
|
+
LPSolve::add_constraint(@lp, p, LPSelect::EQ, 1.0.to_f)
|
212
|
+
end
|
213
|
+
|
214
|
+
LPSolve::set_minim(@lp)
|
215
|
+
solution = LPSolve::solve(@lp)
|
216
|
+
|
217
|
+
assert_nothing_raised do
|
218
|
+
objective = LPSolve::get_objective(@lp)
|
219
|
+
assert_not_nil objective
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# unsigned char get_variables(lprec *lp, REAL *var);
|
224
|
+
def test_get_variables
|
225
|
+
@lp = LPSolve::make_lp(0, 2)
|
226
|
+
LPSolve::set_verbose(@lp, LPSolve::SEVERE )
|
227
|
+
LPSolve::set_col_name(@lp, 1, "fred")
|
228
|
+
LPSolve::set_col_name(@lp, 2, "bob")
|
229
|
+
|
230
|
+
#The API expects a 1 indexed array
|
231
|
+
constraint_vars = [0, 0, 1]
|
232
|
+
FFI::MemoryPointer.new(:double, constraint_vars.size) do |p|
|
233
|
+
p.write_array_of_double(constraint_vars)
|
234
|
+
LPSolve::add_constraint(@lp, p, LPSelect::EQ, 1.0.to_f)
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
LPSolve::set_minim(@lp)
|
239
|
+
solution = LPSolve::solve(@lp)
|
240
|
+
|
241
|
+
retvals = []
|
242
|
+
FFI::MemoryPointer.new(:double, 2) do |p|
|
243
|
+
assert_nothing_raised do
|
244
|
+
err = LPSolve::get_variables(@lp, p)
|
245
|
+
end
|
246
|
+
retvals = p.get_array_of_double(0,2)
|
247
|
+
end
|
248
|
+
assert_not_nil retvals[0]
|
249
|
+
assert_equal 1.0, retvals[1]
|
250
|
+
end
|
251
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lp_select
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jake Sower
|
9
|
+
- James Prior
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2015-07-27 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: ffi
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: bundler
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '1.3'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.3'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
description: Ruby bindings for LPSolve
|
64
|
+
email:
|
65
|
+
- j.sower@asee.org
|
66
|
+
- j.prior@asee.org
|
67
|
+
executables: []
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files: []
|
70
|
+
files:
|
71
|
+
- .gitignore
|
72
|
+
- Gemfile
|
73
|
+
- LICENSE.txt
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- lib/lp_select.rb
|
77
|
+
- lib/lp_select/binaries/liblpsolve55.dylib
|
78
|
+
- lib/lp_select/binaries/liblpsolve55.dylib-ppc
|
79
|
+
- lib/lp_select/binaries/liblpsolve55.dylib.x86-64
|
80
|
+
- lib/lp_select/binaries/liblpsolve55.so
|
81
|
+
- lib/lp_select/binaries/liblpsolve55.so-ux64
|
82
|
+
- lib/lp_select/binaries/lpsolve55.dll
|
83
|
+
- lib/lp_select/lp_select.rb
|
84
|
+
- lib/lp_select/lp_solve.rb
|
85
|
+
- lib/lp_select/version.rb
|
86
|
+
- lp_select.gemspec
|
87
|
+
- test/lp_format_sample.txt
|
88
|
+
- test/lp_select_test.rb
|
89
|
+
- test/lp_solve_test.rb
|
90
|
+
- test/test_helper.rb
|
91
|
+
homepage: https://github.com/asee/lp_select
|
92
|
+
licenses:
|
93
|
+
- LGPL
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 1.8.23
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Ruby bindings for LPSolve
|
116
|
+
test_files:
|
117
|
+
- test/lp_format_sample.txt
|
118
|
+
- test/lp_select_test.rb
|
119
|
+
- test/lp_solve_test.rb
|
120
|
+
- test/test_helper.rb
|
121
|
+
has_rdoc:
|