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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a7e3863747205d299960de14f4f6fb574d435661
4
- data.tar.gz: 5d6eacf431b01313d025970c61b0d1dac0a16b13
3
+ metadata.gz: 494437a765172c4f950dc9f9b0d802b865bca747
4
+ data.tar.gz: 9f8454f7e3891a57cadd13eaba761cf4e13aeaf9
5
5
  SHA512:
6
- metadata.gz: a0d8c29f097fee10546696c1ae8d992286dd96e1c892fec7acdb56a069660bd5c0b539ef04cbdb42e0a6d5c5fe43ca24c0792866604162ae170a6a4aed0c9ed8
7
- data.tar.gz: 3778ce82d42956f7df0c02959e5e20264e6095881e480d617917ad53ff9e24d1f7491e4c54597936b99cb9adfc8f9cbffcba749a1834efda11abd64addb74d7f
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
@@ -1,4 +1,5 @@
1
1
  require 'flt/math'
2
+ require 'flt/tolerance'
2
3
 
3
4
  require 'solver/version'
4
5
  require 'solver/function'
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
- def initialize(context, default_guesses, tol, eqn=nil, &blk)
11
- @context = context
12
- @default_guesses = Array(default_guesses)
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 = eqn || blk
15
- @tol = tol # user-requested tolerance
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
@@ -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
- @solver_class = solver_class || RFSecantSolver
30
- @eqn = blk
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 = nil
35
+ @default_guesses = options[:default_guesses]
35
36
 
36
- @context = context
37
- @tol = 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(@context, @default_guesses, @tol){|v| this.equation_value(v)}
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
@@ -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(context, default_guesses, tol, eqn=nil, &blk)
25
- super context, default_guesses, tol, eqn, &blk
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(context, default_guesses, tol, eqn=nil, &blk)
26
- super context, default_guesses, tol, eqn, &blk
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
@@ -1,5 +1,5 @@
1
1
  module Flt
2
2
  module Solver
3
- VERSION = "0.1.2"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
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.3.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.1.2
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: 2014-05-21 00:00:00.000000000 Z
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.3.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.3.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.rdoc
94
+ - README.md
95
95
  - Rakefile
96
96
  - lib/solver.rb
97
97
  - lib/solver/base.rb
data/README.rdoc DELETED
@@ -1,5 +0,0 @@
1
- = solver
2
-
3
- This numeric solver is an example of the use of Flt.
4
-
5
- Copyright (c) 2010 Javier Goizueta. See LICENSE for details.