solver 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +88 -0
- data/lib/solver.rb +1 -0
- data/lib/solver/base.rb +44 -8
- data/lib/solver/psolver.rb +18 -14
- data/lib/solver/rfsecant.rb +7 -7
- data/lib/solver/secant.rb +7 -8
- data/lib/solver/tvm.rb +3 -3
- data/lib/solver/version.rb +1 -1
- data/solver.gemspec +1 -1
- data/test/test_secant.rb +26 -0
- metadata +5 -5
- data/README.rdoc +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 494437a765172c4f950dc9f9b0d802b865bca747
|
4
|
+
data.tar.gz: 9f8454f7e3891a57cadd13eaba761cf4e13aeaf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f152b61a7089fb97991f7624d97814f27a149b964f717e54ff397a942441beb0302d8dde8320015ee7445407ffaf6afbd3b45810a1f042bf0272372b3dc1f9b
|
7
|
+
data.tar.gz: 94b62848a1e693e6b2a51d55f8a29bf3c6835ccdc60ef131cc50cb537cbf07f3ae285e5c9ed875be7d9632456c77b2ecce38d6c7124be29438d27393231bdd0a
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# solver
|
2
|
+
|
3
|
+
This numeric solver is an example of the use of Flt.
|
4
|
+
|
5
|
+
## Examples
|
6
|
+
|
7
|
+
Solve equation `2*exp(x)-10=0` using Float, with a tolerance of 10 decimal places,
|
8
|
+
using the `SecantSolver` algorithm and with initial guesses of 0, 10:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
require 'solver'
|
12
|
+
equation = ->(x){ 2*exp(x)-10 }
|
13
|
+
algorithm = Flt::Solver::SecantSolver
|
14
|
+
tolerance = Flt::Tolerance(10, :decimals)
|
15
|
+
solver = algorithm.new(Float.context, tolerance, &equation)
|
16
|
+
puts solver.root(0, 10) # => 1.6094379124341003
|
17
|
+
```
|
18
|
+
|
19
|
+
Now, solve the same equation, with the same algorithm, but requiring greater
|
20
|
+
accuracy and using a higher precision numeric type:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
tolerance = Flt::Tolerance(25, :decimals)
|
24
|
+
solver = algorithm.new(Flt::DecNum.context(precision: 30), tolerance, &equation)
|
25
|
+
puts solver.root(0, 10) # => DecNum('1.609437912434100374600759333')
|
26
|
+
```
|
27
|
+
|
28
|
+
Let's compute the same using a different algorithm:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
algorithm = Flt::Solver::RFSecantSolver
|
32
|
+
solver = algorithm.new(Flt::DecNum.context(precision: 30), tolerance, &equation)
|
33
|
+
puts solver.root(0, 10) # => DecNum('1.609437912434100374600759333')
|
34
|
+
```
|
35
|
+
|
36
|
+
The `PSolver` class can be used to define an equation with multiple parameters, then
|
37
|
+
solve for any of them.
|
38
|
+
|
39
|
+
Let's define an equation to compute the *Time Value of Money* (i.e. compound interest):
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
tvm_equation = ->(future_value, time, present_value, payment, interest, payments_per_year) {
|
43
|
+
i = interest/100/payments_per_year
|
44
|
+
n = -time
|
45
|
+
k = (i + 1)**n # not the right way to compute it!
|
46
|
+
present_value + payment*(1-k)/i + future_value*k
|
47
|
+
}
|
48
|
+
```
|
49
|
+
|
50
|
+
Note that computing `(i + 1)**n` as in this example is not a good idea, see the `TVM`
|
51
|
+
class for a better approach.
|
52
|
+
|
53
|
+
Now let's use `PSolver` to solve this equation for any of its paramters:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
tvm_solver = Flt::Solver::PSolver.new(
|
57
|
+
Flt::DecNum.context,
|
58
|
+
Flt.Tolerance(2,:decimals),
|
59
|
+
&tvm_equation
|
60
|
+
)
|
61
|
+
```
|
62
|
+
|
63
|
+
For example, let's compute the monthly payment of a loan of $150,000, to be paid in twenty years
|
64
|
+
with a 7% interest rate:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
pmt = tvm_solver.root(
|
68
|
+
:payment,
|
69
|
+
present_value: 150000,
|
70
|
+
future_value: 0,
|
71
|
+
time: 20*12,
|
72
|
+
interest: 7,
|
73
|
+
payments_per_year: 12
|
74
|
+
)
|
75
|
+
puts s.round(2) # => -1162.95
|
76
|
+
```
|
77
|
+
|
78
|
+
A better (more accurate) financial solver is implemented by the `TVM` class:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
tvm = Flt::Solver::TVM.new(Flt.Tolerance(2,:decimals), Flt::DecNum.context)
|
82
|
+
solution = tvm.solve(t: 20*12, m0: 150000, m: 0, i: 7, p: 12)
|
83
|
+
puts solution[:pmt].round(2) # => -1162.95
|
84
|
+
```
|
85
|
+
|
86
|
+
# Licensing
|
87
|
+
|
88
|
+
Copyright (c) 2010 Javier Goizueta. See LICENSE for details.
|
data/lib/solver.rb
CHANGED
data/lib/solver/base.rb
CHANGED
@@ -6,13 +6,27 @@ module Flt::Solver
|
|
6
6
|
|
7
7
|
class Base
|
8
8
|
|
9
|
+
# Admitted options:
|
10
|
+
#
|
11
|
+
# * :context : Numerical context (e.g. Flt::Decimal::Context) Float by default
|
12
|
+
# * :default_guesses : default initial guesses
|
13
|
+
# * :tolerance : numerical tolerance
|
14
|
+
# * :equation : equation to be solved; can also be passed as a block
|
15
|
+
#
|
9
16
|
# default_guesses: nil for no pre-guess, or one or two guesses (use array for two)
|
10
|
-
|
11
|
-
|
12
|
-
|
17
|
+
#
|
18
|
+
# The values of any of the parameters can also be passed as arguments
|
19
|
+
# (not in the options Hash, if present) in any order, e.g.:
|
20
|
+
#
|
21
|
+
# Solver::Base.new Flt::DecNum.context, tolerance: Flt::Tolerance(3, :decimals)
|
22
|
+
#
|
23
|
+
def initialize(*args, &blk)
|
24
|
+
options = Base.extract_options(*args, &blk)
|
25
|
+
@context = options[:context] || Float.context
|
26
|
+
@default_guesses = Array(options[:default_guesses])
|
13
27
|
@x = @default_guesses.first
|
14
|
-
@f =
|
15
|
-
@tol =
|
28
|
+
@f = options[:equation]
|
29
|
+
@tol = options[:tolerance] # user-requested tolerance
|
16
30
|
@max_it = 8192
|
17
31
|
reset
|
18
32
|
end
|
@@ -25,8 +39,6 @@ class Base
|
|
25
39
|
@conv = false
|
26
40
|
end
|
27
41
|
|
28
|
-
# value of parameters[var] is used as a guess in precedence to the pre-guesses if not nil
|
29
|
-
# use Array for two guesses
|
30
42
|
def root(*guesses)
|
31
43
|
@guess = (guesses + @default_guesses).map{|g| num(g)}
|
32
44
|
reset
|
@@ -36,7 +48,7 @@ class Base
|
|
36
48
|
@conv = false
|
37
49
|
|
38
50
|
# Minimum tolerance of the numeric type used
|
39
|
-
@numeric_tol = Tolerance(1,:ulps) # Tolerance(@context.epsilon, :floating)
|
51
|
+
@numeric_tol = Flt::Tolerance(1,:ulps) # Tolerance(@context.epsilon, :floating)
|
40
52
|
|
41
53
|
raise "Invalid parameters" unless validate
|
42
54
|
|
@@ -107,6 +119,30 @@ class Base
|
|
107
119
|
true
|
108
120
|
end
|
109
121
|
|
122
|
+
def self.extract_options(*args, &blk)
|
123
|
+
options = {}
|
124
|
+
args.each do |arg|
|
125
|
+
case arg
|
126
|
+
when Hash
|
127
|
+
options.merge! arg
|
128
|
+
when Flt::Num::ContextBase, Flt::FloatContext
|
129
|
+
options.merge! context: arg
|
130
|
+
when Array, Numeric
|
131
|
+
options.merge! default_guesses: Array(arg)
|
132
|
+
when Proc
|
133
|
+
options.merge! equation: arg
|
134
|
+
when Flt::Tolerance
|
135
|
+
options.merge! tolerance: arg
|
136
|
+
when Base
|
137
|
+
options.merge! solver_class: arg
|
138
|
+
else
|
139
|
+
raise "Invalid Argument #{arg.inspect}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
options[:equation] ||= blk
|
143
|
+
options
|
144
|
+
end
|
145
|
+
|
110
146
|
end # Base
|
111
147
|
|
112
148
|
end # Flt::Solve
|
data/lib/solver/psolver.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Flt::Solver
|
2
|
-
|
2
|
+
|
3
3
|
# Solver with equation parameters passed in a hash
|
4
|
-
#
|
4
|
+
#
|
5
5
|
# Example: a simple TVM (Time Value of Money) solver
|
6
6
|
#
|
7
7
|
# # ln(x+1)
|
@@ -9,7 +9,7 @@ module Flt::Solver
|
|
9
9
|
# v = x + 1
|
10
10
|
# (v == 1) ? x : (x*ln(v) / (v - 1))
|
11
11
|
# end
|
12
|
-
#
|
12
|
+
#
|
13
13
|
# tvm = Flt::Solver::PSolver.new(Float.context, Flt.Tolerance(3,:decimals)) do |m, t, m0, pmt, i, p|
|
14
14
|
# i /= 100
|
15
15
|
# i /= p
|
@@ -23,20 +23,20 @@ module Flt::Solver
|
|
23
23
|
# puts sol.inspect # => -55.45975978539105
|
24
24
|
#
|
25
25
|
class PSolver
|
26
|
-
|
27
|
-
def initialize(context, tol, solver_class=nil, &blk)
|
28
26
|
|
29
|
-
|
30
|
-
|
27
|
+
def initialize(*args, &blk)
|
28
|
+
options = Base.extract_options(*args, &blk)
|
29
|
+
|
30
|
+
@solver_class = options[:solver_class] || RFSecantSolver
|
31
|
+
@eqn = options[:equation]
|
31
32
|
@vars = Function.parameters(@eqn)
|
32
33
|
# alternatively, we could keep @eqn = Function[@eqn] and dispense with @vars
|
33
34
|
|
34
|
-
@default_guesses =
|
35
|
+
@default_guesses = options[:default_guesses]
|
35
36
|
|
36
|
-
@context = context
|
37
|
-
@tol =
|
37
|
+
@context = options[:context]
|
38
|
+
@tol = options[:tolerance]
|
38
39
|
@solver = nil
|
39
|
-
|
40
40
|
end
|
41
41
|
|
42
42
|
def default_guesses=(*g)
|
@@ -55,16 +55,20 @@ module Flt::Solver
|
|
55
55
|
def equation_value(v)
|
56
56
|
values = @parameters.merge(@var=>v)
|
57
57
|
#@context.math(*values.values_at(*@vars).map{|v| @context.Num(v)}, &@eqn)
|
58
|
-
@context.math(*@vars.map{|v| @context.Num(values[v])}, &@eqn)
|
58
|
+
@context.math(*@vars.map{|v| values[v] && @context.Num(values[v])}.compact, &@eqn)
|
59
59
|
# equivalent to: @context.math(values, &Function[@eqn]) # which doesn't need @vars
|
60
60
|
end
|
61
61
|
|
62
62
|
private
|
63
63
|
def init_solver
|
64
64
|
this = self
|
65
|
-
@solver ||= @solver_class.new(
|
65
|
+
@solver ||= @solver_class.new(
|
66
|
+
context: @context,
|
67
|
+
default_guesses: @default_guesses,
|
68
|
+
tolerance: @tol
|
69
|
+
) { |v| this.equation_value(v) }
|
66
70
|
end
|
67
71
|
|
68
72
|
end
|
69
73
|
|
70
|
-
end # Flt::PSolver
|
74
|
+
end # Flt::PSolver
|
data/lib/solver/rfsecant.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Flt::Solver
|
2
|
-
|
2
|
+
|
3
3
|
# Regula-Falsi/Secant method solver
|
4
4
|
# Secant is used if no bracketing is available
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# Example of use:
|
7
7
|
# require 'solver'
|
8
8
|
# include Flt
|
@@ -21,12 +21,12 @@ module Flt::Solver
|
|
21
21
|
#
|
22
22
|
class RFSecantSolver < Base
|
23
23
|
|
24
|
-
def initialize(
|
25
|
-
super
|
24
|
+
def initialize(*args, &blk)
|
25
|
+
super
|
26
26
|
@half = num(Rational(1,2))
|
27
27
|
reset
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
def reset
|
31
31
|
super
|
32
32
|
@a = @b = @fa = @fb = nil
|
@@ -85,5 +85,5 @@ module Flt::Solver
|
|
85
85
|
end
|
86
86
|
|
87
87
|
end # RFSecantSolver
|
88
|
-
|
89
|
-
end # Flt::Solver
|
88
|
+
|
89
|
+
end # Flt::Solver
|
data/lib/solver/secant.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
module Flt::Solver
|
2
|
-
|
2
|
+
|
3
3
|
# Secant method solver
|
4
4
|
# Bisect method is used is bracketing found (sign(f(a)) != sign(f(b)))
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# Example of use:
|
7
7
|
# require 'solver'
|
8
|
-
# require 'flt/tolerance'
|
9
8
|
# include Flt
|
10
9
|
# solver = Solver::SecantSolver.new(Float.context, [0.0, 100.0], Tolerance(3, :decimals)) do |x|
|
11
10
|
# 2*x+11.0
|
@@ -22,12 +21,12 @@ module Flt::Solver
|
|
22
21
|
#
|
23
22
|
class SecantSolver < Base
|
24
23
|
|
25
|
-
def initialize(
|
26
|
-
super
|
24
|
+
def initialize(*args, &blk)
|
25
|
+
super
|
27
26
|
@half = num(Rational(1,2))
|
28
27
|
reset
|
29
28
|
end
|
30
|
-
|
29
|
+
|
31
30
|
def reset
|
32
31
|
super
|
33
32
|
@a = @b = @fa = @fb = nil
|
@@ -85,5 +84,5 @@ module Flt::Solver
|
|
85
84
|
end
|
86
85
|
|
87
86
|
end # SecantSolver
|
88
|
-
|
89
|
-
end # Flt::Solver
|
87
|
+
|
88
|
+
end # Flt::Solver
|
data/lib/solver/tvm.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
module Flt::Solver
|
2
2
|
|
3
3
|
# A Time-Value-of-Money solver
|
4
|
-
#
|
4
|
+
#
|
5
5
|
# Example:
|
6
6
|
# tvm = TVM.new(Tolerance(3, :decimals), Float.context)
|
7
7
|
# puts tvm.solve(:t=>240, :m0=>10000, :m=>0, :i=>3, :p=>12).inspect # => {:pmt=>-55.45975978539105}
|
8
|
-
#
|
8
|
+
#
|
9
9
|
class TVM
|
10
10
|
|
11
11
|
def initialize(tol, context=Float.context)
|
@@ -69,4 +69,4 @@ module Flt::Solver
|
|
69
69
|
|
70
70
|
end # TVM
|
71
71
|
|
72
|
-
end # Flt::Solver
|
72
|
+
end # Flt::Solver
|
data/lib/solver/version.rb
CHANGED
data/solver.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_runtime_dependency "flt", '~> 1.
|
21
|
+
spec.add_runtime_dependency "flt", '~> 1.5'
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.6"
|
23
23
|
spec.add_development_dependency "rake"
|
24
24
|
spec.add_development_dependency "minitest"
|
data/test/test_secant.rb
CHANGED
@@ -32,6 +32,32 @@ class TestSecant < MiniTest::Unit::TestCase
|
|
32
32
|
assert_in_delta 1.6094389956808506, solver.root(1.0), @delta
|
33
33
|
assert_in_delta 1.6094389956808506, solver.root(2.0), @delta
|
34
34
|
end
|
35
|
+
|
36
|
+
should "be flexible with the solver parameters" do
|
37
|
+
solver = Flt::Solver::SecantSolver.new(@context, default_guesses: [0.0, 10.0], tolerance: @tolerance) do |x|
|
38
|
+
y = 2
|
39
|
+
y*exp(x)-10
|
40
|
+
end
|
41
|
+
assert_in_delta 1.6094389956808506, solver.root, @delta
|
42
|
+
assert_in_delta 1.6094389956808506, solver.root(1.0), @delta
|
43
|
+
assert_in_delta 1.6094389956808506, solver.root(2.0), @delta
|
44
|
+
|
45
|
+
solver = Flt::Solver::SecantSolver.new(context: @context, default_guesses: [0.0, 10.0], tolerance: @tolerance) do |x|
|
46
|
+
y = 2
|
47
|
+
y*exp(x)-10
|
48
|
+
end
|
49
|
+
assert_in_delta 1.6094389956808506, solver.root, @delta
|
50
|
+
assert_in_delta 1.6094389956808506, solver.root(1.0), @delta
|
51
|
+
assert_in_delta 1.6094389956808506, solver.root(2.0), @delta
|
52
|
+
|
53
|
+
solver = Flt::Solver::SecantSolver.new(@tolerance, @context, [0.0, 10.0]) do |x|
|
54
|
+
y = 2
|
55
|
+
y*exp(x)-10
|
56
|
+
end
|
57
|
+
assert_in_delta 1.6094389956808506, solver.root, @delta
|
58
|
+
assert_in_delta 1.6094389956808506, solver.root(1.0), @delta
|
59
|
+
assert_in_delta 1.6094389956808506, solver.root(2.0), @delta
|
60
|
+
end
|
35
61
|
end
|
36
62
|
|
37
63
|
context "using DecNum arithmetic" do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Javier Goizueta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-06-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: flt
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.
|
19
|
+
version: '1.5'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.
|
26
|
+
version: '1.5'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -91,7 +91,7 @@ files:
|
|
91
91
|
- ".gitignore"
|
92
92
|
- Gemfile
|
93
93
|
- LICENSE
|
94
|
-
- README.
|
94
|
+
- README.md
|
95
95
|
- Rakefile
|
96
96
|
- lib/solver.rb
|
97
97
|
- lib/solver/base.rb
|