solver 0.1.2 → 0.2.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 +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
|