lpsolver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +345 -0
- data/exe/README.md +1 -0
- data/lib/lpsolver/constraint_spec.rb +96 -0
- data/lib/lpsolver/exception.rb +55 -0
- data/lib/lpsolver/linear_expression.rb +194 -0
- data/lib/lpsolver/model.rb +520 -0
- data/lib/lpsolver/quadratic_expression.rb +164 -0
- data/lib/lpsolver/solution.rb +123 -0
- data/lib/lpsolver/variable.rb +221 -0
- data/lib/lpsolver/version.rb +8 -0
- data/lib/lpsolver.rb +41 -0
- data/lpsolver.gemspec +29 -0
- metadata +65 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LpSolver
|
|
4
|
+
# Represents a linear expression: sum of (coefficient * variable) + constant.
|
|
5
|
+
#
|
|
6
|
+
# Linear expressions are the fundamental building blocks of linear programming
|
|
7
|
+
# models. They represent quantities like costs, resource usage, or returns
|
|
8
|
+
# that scale linearly with decision variables.
|
|
9
|
+
#
|
|
10
|
+
# A linear expression has the mathematical form:
|
|
11
|
+
# c₀ + c₁·x₁ + c₂·x₂ + ... + cₙ·xₙ
|
|
12
|
+
#
|
|
13
|
+
# where c₀ is the constant offset and cᵢ are the coefficients for each variable.
|
|
14
|
+
#
|
|
15
|
+
# @example Building a linear expression
|
|
16
|
+
# x = model.add_variable(:x, lb: 0)
|
|
17
|
+
# y = model.add_variable(:y, lb: 0)
|
|
18
|
+
# expr = x * 2 + y * 3 + 5 # 2x + 3y + 5
|
|
19
|
+
# expr.terms # => {0 => 2.0, 1 => 3.0}
|
|
20
|
+
# expr.constant # => 5.0
|
|
21
|
+
#
|
|
22
|
+
# @example Using in constraints
|
|
23
|
+
# model.add_constraint(:budget, (x * 2 + y * 3 + 5) <= 100)
|
|
24
|
+
#
|
|
25
|
+
# @example Using in objectives
|
|
26
|
+
# model.set_objective(x * 2 + y * 3 + 5)
|
|
27
|
+
class LinearExpression
|
|
28
|
+
# @return [Hash{Integer => Float}] Maps variable indices to their coefficients.
|
|
29
|
+
# The keys are the internal indices assigned by the model, and the values
|
|
30
|
+
# are the floating-point coefficients.
|
|
31
|
+
# @example
|
|
32
|
+
# (x * 2 + y * 3).terms # => {0 => 2.0, 1 => 3.0}
|
|
33
|
+
attr_reader :terms
|
|
34
|
+
|
|
35
|
+
# @return [Float] The constant offset in the expression.
|
|
36
|
+
# @example
|
|
37
|
+
# (x * 2 + 5).constant # => 5.0
|
|
38
|
+
attr_reader :constant
|
|
39
|
+
|
|
40
|
+
# Creates a new LinearExpression.
|
|
41
|
+
#
|
|
42
|
+
# @param terms [Hash{Integer => Float}] Maps variable indices to coefficients.
|
|
43
|
+
# @param constant [Float] The constant offset (default: 0.0).
|
|
44
|
+
def initialize(terms = {}, constant = 0.0)
|
|
45
|
+
@terms = terms
|
|
46
|
+
@constant = constant
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Adds another expression, variable, constant, or quadratic expression.
|
|
50
|
+
#
|
|
51
|
+
# When adding a LinearExpression or Variable, returns a new LinearExpression.
|
|
52
|
+
# When adding a QuadraticExpression, returns a new QuadraticExpression
|
|
53
|
+
# that combines the linear and quadratic parts.
|
|
54
|
+
#
|
|
55
|
+
# @param other [LinearExpression, QuadraticExpression, Variable, Numeric] The operand to add.
|
|
56
|
+
# @return [LinearExpression] When +other+ is a Numeric, Variable, or LinearExpression.
|
|
57
|
+
# @return [QuadraticExpression] When +other+ is a QuadraticExpression.
|
|
58
|
+
# @example Adding two linear expressions
|
|
59
|
+
# (x * 2 + 3) + (x + 5) # => 3x + 8
|
|
60
|
+
# @example Adding a quadratic expression
|
|
61
|
+
# (x * 2) + (y * y) # => QuadraticExpression with linear: {x => 2}, quadratic: [[y, y, 1]]
|
|
62
|
+
def +(other)
|
|
63
|
+
if other.is_a?(Variable)
|
|
64
|
+
LinearExpression.new(
|
|
65
|
+
merge_terms(@terms, { other.index => 1.0 }),
|
|
66
|
+
@constant
|
|
67
|
+
)
|
|
68
|
+
elsif other.is_a?(LinearExpression)
|
|
69
|
+
LinearExpression.new(
|
|
70
|
+
merge_terms(@terms, other.terms),
|
|
71
|
+
@constant + other.constant
|
|
72
|
+
)
|
|
73
|
+
elsif other.is_a?(QuadraticExpression)
|
|
74
|
+
new_linear = other.linear_terms.dup
|
|
75
|
+
@terms.each { |idx, coeff| new_linear[idx] = (new_linear[idx] || 0) + coeff }
|
|
76
|
+
QuadraticExpression.new(new_linear.reject { |_, v| v.zero? }, other.quadratic_terms.dup)
|
|
77
|
+
else
|
|
78
|
+
LinearExpression.new(@terms.dup, @constant + other.to_f)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Subtracts another expression, variable, constant, or quadratic expression.
|
|
83
|
+
#
|
|
84
|
+
# @param other [LinearExpression, QuadraticExpression, Variable, Numeric] The operand to subtract.
|
|
85
|
+
# @return [LinearExpression] When +other+ is a Numeric, Variable, or LinearExpression.
|
|
86
|
+
# @return [QuadraticExpression] When +other+ is a QuadraticExpression.
|
|
87
|
+
# @example Subtracting a linear expression
|
|
88
|
+
# (x * 5 + 10) - (x * 2 + 3) # => 3x + 7
|
|
89
|
+
def -(other)
|
|
90
|
+
if other.is_a?(Variable)
|
|
91
|
+
LinearExpression.new(
|
|
92
|
+
merge_terms(@terms, { other.index => -1.0 }),
|
|
93
|
+
@constant
|
|
94
|
+
)
|
|
95
|
+
elsif other.is_a?(LinearExpression)
|
|
96
|
+
LinearExpression.new(
|
|
97
|
+
merge_terms(@terms, negate_terms(other.terms)),
|
|
98
|
+
@constant - other.constant
|
|
99
|
+
)
|
|
100
|
+
elsif other.is_a?(QuadraticExpression)
|
|
101
|
+
new_linear = other.linear_terms.dup
|
|
102
|
+
@terms.each { |idx, coeff| new_linear[idx] = (new_linear[idx] || 0) + coeff }
|
|
103
|
+
neg_quad = other.quadratic_terms.map { |i1, i2, c| [i1, i2, -c] }
|
|
104
|
+
QuadraticExpression.new(new_linear.reject { |_, v| v.zero? }, neg_quad)
|
|
105
|
+
else
|
|
106
|
+
LinearExpression.new(@terms.dup, @constant - other.to_f)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Multiplies this expression by a scalar.
|
|
111
|
+
#
|
|
112
|
+
# Scales both the variable coefficients and the constant by the given factor.
|
|
113
|
+
#
|
|
114
|
+
# @param scalar [Numeric] The scalar multiplier.
|
|
115
|
+
# @return [LinearExpression] A new expression with all coefficients scaled.
|
|
116
|
+
# @example
|
|
117
|
+
# (x * 2 + 3) * 4 # => 8x + 12
|
|
118
|
+
# @param [Object] other
|
|
119
|
+
def *(other)
|
|
120
|
+
s = other.to_f
|
|
121
|
+
LinearExpression.new(
|
|
122
|
+
@terms.transform_values { |c| c * s },
|
|
123
|
+
@constant * s
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Returns a LinearExpression with all coefficients and the constant negated.
|
|
128
|
+
#
|
|
129
|
+
# @return [LinearExpression] A new expression with negated terms.
|
|
130
|
+
# @example
|
|
131
|
+
# -(x * 3 + 5) # => -3x - 5
|
|
132
|
+
def -@
|
|
133
|
+
LinearExpression.new(
|
|
134
|
+
@terms.transform_values { |c| -c },
|
|
135
|
+
-@constant
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Creates a less-than-or-equal-to constraint specification.
|
|
140
|
+
#
|
|
141
|
+
# @param value [Numeric] The right-hand side upper bound.
|
|
142
|
+
# @return [ConstraintSpec] A constraint specification with operator :le.
|
|
143
|
+
# @example
|
|
144
|
+
# model.add_constraint(:c, (x * 2 + y * 3 + 5) <= 100)
|
|
145
|
+
# @param [Object] other
|
|
146
|
+
def <=(other)
|
|
147
|
+
ConstraintSpec.new(:le, @terms.dup, @constant, other.to_f)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Creates a greater-than-or-equal-to constraint specification.
|
|
151
|
+
#
|
|
152
|
+
# @param value [Numeric] The right-hand side lower bound.
|
|
153
|
+
# @return [ConstraintSpec] A constraint specification with operator :ge.
|
|
154
|
+
# @example
|
|
155
|
+
# model.add_constraint(:c, (x * 2 + y * 3 + 5) >= 50)
|
|
156
|
+
# @param [Object] other
|
|
157
|
+
def >=(other)
|
|
158
|
+
ConstraintSpec.new(:ge, @terms.dup, @constant, other.to_f)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Creates an equality constraint specification.
|
|
162
|
+
#
|
|
163
|
+
# @param value [Numeric] The exact value the expression must equal.
|
|
164
|
+
# @return [ConstraintSpec] A constraint specification with operator :eq.
|
|
165
|
+
# @example
|
|
166
|
+
# model.add_constraint(:c, (x * 2 + y * 3 + 5) == 75)
|
|
167
|
+
# @param [Object] other
|
|
168
|
+
def ==(other)
|
|
169
|
+
ConstraintSpec.new(:eq, @terms.dup, @constant, other.to_f)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Merges two term hashes, combining coefficients for duplicate indices.
|
|
175
|
+
#
|
|
176
|
+
# @param a [Hash{Integer => Float}] First term hash.
|
|
177
|
+
# @param b [Hash{Integer => Float}] Second term hash.
|
|
178
|
+
# @return [Hash{Integer => Float}] The merged term hash with zero coefficients removed.
|
|
179
|
+
def merge_terms(a, b)
|
|
180
|
+
merged = a.dup
|
|
181
|
+
b.each { |idx, coeff| merged[idx] = (merged[idx] || 0) + coeff }
|
|
182
|
+
merged.reject! { |_, v| v.zero? }
|
|
183
|
+
merged
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Negates all coefficients in a term hash.
|
|
187
|
+
#
|
|
188
|
+
# @param terms [Hash{Integer => Float}] The term hash to negate.
|
|
189
|
+
# @return [Hash{Integer => Float}] A new hash with negated coefficients.
|
|
190
|
+
def negate_terms(terms)
|
|
191
|
+
terms.transform_values { |c| -c }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|