symbolic 0.3.7 → 0.3.8

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.
@@ -2,14 +2,38 @@ module Symbolic::Math
2
2
  =begin
3
3
  This module is a reflection for Math module which allows to use symbolic expressions as parameters for its methods.
4
4
  =end
5
- Math.methods(false).each do |method|
5
+ require "#{File.dirname(__FILE__)}/function.rb"
6
+ require 'rational'
7
+
8
+ #for use in defining derivatives
9
+ Arg = Symbolic::Variable.new(:name=>'Arg')
10
+
11
+ #first, make the functions with derivatives
12
+ Abs = Symbolic::Function.new('abs', proc{|arg| arg/Abs[arg]}){|arg| arg.abs}
13
+ Sqrt = Symbolic::Function.new('sqrt', Rational(1,2) / Arg ** Rational(1,2))
14
+ Exp = Symbolic::Function.new('exp'); Exp.set_derivative(Exp)
15
+ Log = Symbolic::Function.new('log', 1 / Arg)
16
+ Log10 = Symbolic::Function.new('log10', 1 / Arg / ::Math.log(10)) #since log10(x) = log(x) / log(10)
17
+ Cos = Symbolic::Function.new('cos')
18
+ Sin = Symbolic::Function.new('sin',Cos); Cos.set_derivative(-Sin[Arg])
19
+ Tan = Symbolic::Function.new('tan', 1 / Cos[Arg] ** 2)
20
+ Cosh = Symbolic::Function.new('cosh')
21
+ Sinh = Symbolic::Function.new('sinh',Cosh); Cosh.set_derivative(Sinh)
22
+ Tanh = Symbolic::Function.new('tanh',1 / Cosh[Arg] ** 2)
23
+ Acos = Symbolic::Function.new('acos',- 1 / (1 - Arg) ** Rational(1,2))
24
+ Asin = Symbolic::Function.new('asin',1 / (1 - Arg) ** Rational(1,2))
25
+ Atan = Symbolic::Function.new('atan',1 / (Arg**2 + 1))
26
+ Acosh = Symbolic::Function.new('acosh',1 / (1 - Arg) ** Rational(1,2))
27
+ Asinh = Symbolic::Function.new('asinh',1 / (1 + Arg) ** Rational(1,2))
28
+ Atanh = Symbolic::Function.new('atanh',1/ (1 - Arg**2))
29
+
30
+ #make functions of the form fctn(arg) and add operation to each function
31
+ #for ruby 1.9, we have to convert them to strings (they were strings in 1.8)
32
+ Symbolic::Math.constants.collect{|c| c.to_s}.reject{|c| ['Arg','Abs'].include?(c)}.each do |fctn|
6
33
  instance_eval <<-CODE, __FILE__, __LINE__ + 1
7
- def #{method}(argument)
8
- unless argument.is_a? Numeric
9
- Symbolic::Function.new argument, :#{method}
10
- else
11
- ::Math.#{method} argument
12
- end
34
+ #{fctn}.set_operation(proc{|arg| ::Math.#{fctn.downcase}(arg)})
35
+ def #{fctn.downcase}(argument)
36
+ #{fctn}[argument]
13
37
  end
14
38
  CODE
15
39
  end
@@ -0,0 +1,5 @@
1
+ module Symbolic
2
+ def factorial(n)
3
+ (1..n).inject(1){|f,n| f*n}
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ # This is a simple module to give the name of the Ruby variable to the Symbolic::Variable
2
+ # x = var #=> x = var :name => 'x')
3
+ # It can also name multiple variables (but with no options then):
4
+ # x, y = vars
5
+ # This works with caller, and then need to be called directly
6
+
7
+ module Kernel
8
+ alias :_var :var
9
+ def var(options={}, &proc)
10
+ unless options.has_key? :name
11
+ file, ln = caller[0].split(':')
12
+
13
+ options.merge!(
14
+ :name => File.open(file) { |f|
15
+ f.each_line.take(ln.to_i)[-1]
16
+ }.match(/
17
+ \s*([[:word:]]+)
18
+ \s*=
19
+ \s*var/x
20
+ ) {
21
+ $1
22
+ })
23
+ end
24
+ _var(options, &proc)
25
+ end
26
+
27
+ def vars
28
+ file, ln = caller[0].split(':')
29
+
30
+ File.open(file) { |f|
31
+ f.each_line.take(ln.to_i)[-1]
32
+ }.match(/
33
+ ((?:\s*[[:word:]]+?,?)+)
34
+ \s*=
35
+ \s*vars/x
36
+ ) {
37
+ $1
38
+ }.scan(/([[:word:]]+)/).map { |capture|
39
+ _var(:name => capture[0])
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,79 @@
1
+ =begin
2
+ This class intend to handle the String representation of a Symbolic expression
3
+ Two formats will be soon supported: standard and LaTeX
4
+ =end
5
+ module Symbolic
6
+ class Printer
7
+ class << self
8
+ def print(o)
9
+ send(o.class.simple_name.downcase, o)
10
+ end
11
+
12
+ def brackets(var)
13
+ [Numeric, Variable, Function].any? { |c| var.is_a? c } ? var.to_s : "(#{var})"
14
+ end
15
+
16
+ def rational(r)
17
+ "#{r.round == r ? r.to_i : r.to_f}"
18
+ end
19
+
20
+ def coef(c)
21
+ "#{'-' if c < 0}#{"#{rational c.abs}*" if c.abs != 1}"
22
+ end
23
+ def coef_with_sign(c)
24
+ "#{ c < 0 ? '-' : '+'}#{"#{rational c.abs}*" if c.abs != 1}"
25
+ end
26
+
27
+ # Factors
28
+ def factors(f)
29
+ rfactors, factors = f.symbolic.partition { |b,e| e.is_a?(Numeric) && e < 0 }
30
+ rfactors = rfactors.empty? ? nil : [ 1, Hash[*rfactors.flatten] ]
31
+ factors = factors.empty? ? nil : [ f.numeric, Hash[*factors.flatten] ]
32
+
33
+ s = (factors ? output(factors) : rational(f.numeric))
34
+ s << "/#{reversed_output rfactors}" if rfactors
35
+ s
36
+ end
37
+ def output(f) # This has to change ! can be inline in ::factors, but needed also in reversed_output
38
+ coef(f[0]) << f[1].map {|b,e| exponent b,e }.join('*')
39
+ end
40
+ def reversed_output(f) # This has to change ! Please make this non dependent of output ;)
41
+ result = output [f[0], Hash[*f[1].map {|b,e| [b,-e] }.flatten]]
42
+ (f[1].length > 1) ? "(#{result})" : result
43
+ end
44
+ def exponent(base, exponent)
45
+ "#{brackets base}#{"**#{brackets exponent}" if exponent != 1}"
46
+ end
47
+
48
+ # Summands
49
+ def summands(s)
50
+ out = s.symbolic.map { |base, coef| coef_with_sign(coef) + brackets(base) }
51
+ out << remainder(s.numeric)
52
+ out[0].sub!(/^\+/, '')
53
+ out.join
54
+ end
55
+
56
+ def remainder(n)
57
+ "#{'+' if n > 0}#{n unless n.zero?}"
58
+ end
59
+
60
+ # Variable
61
+ def variable(v)
62
+ "#{v.name || :unnamed_variable}"
63
+ end
64
+
65
+ # Function
66
+ def functionwrapper(f)
67
+ "#{f.name}(#{f.argument})"
68
+ end
69
+
70
+ def constant(c)
71
+ "#{c.name || :unnamed_variable}"
72
+ end
73
+ # Sums
74
+ def sum(s)
75
+ "Sum(#{s.term}, #{s.index} = #{s.lb}..#{s.ub})"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,13 +1,18 @@
1
1
  module Symbolic
2
+ OPERATIONS = [:+, :-, :*, :/, :**, :-@]
2
3
  def operations
3
4
  formula = to_s
4
- stats = {}
5
- stats['+'] = formula.scan(/\+/).size
6
- stats['-'] = formula.scan(/[^(]-/).size
7
- stats['*'] = formula.scan(/[^*]\*[^*]/).size
8
- stats['/'] = formula.scan(/\//).size
9
- stats['**']= formula.scan(/\*\*/).size
10
- stats['-@']= formula.scan(/\(-/).size + formula.scan(/^-/).size
11
- stats
5
+ OPERATIONS.inject({}) { |stats, op|
6
+ stats.merge({
7
+ op => formula.scan(
8
+ case op
9
+ when :- then /[^(]-/
10
+ when :* then /[^*]\*[^*]/
11
+ when :-@ then /\(-|^-/
12
+ else /#{Regexp.escape(op.to_s)}/
13
+ end
14
+ ).size
15
+ })
16
+ }
12
17
  end
13
18
  end
@@ -0,0 +1,43 @@
1
+ module Symbolic::Misc
2
+ =begin
3
+ blah
4
+ =end
5
+ class Sum
6
+ attr_reader :term, :index, :lb, :ub
7
+ include Symbolic
8
+ def initialize(term,index,lb,ub)
9
+ @term, @index, @lb, @ub = term, index, lb, ub
10
+ end
11
+ def Sum.[](term,index,lb,ub)
12
+ Symbolic::Sum.new(term,index,lb,ub)
13
+ end
14
+ def expand
15
+ (lb..ub).collect{|ind| @term.subs(@index,ind)}.inject{|m,t| m + t}
16
+ end
17
+ def variables
18
+ @term.variables.reject{|var| var == @index}
19
+ end
20
+ def diff(wrt)
21
+ #TODO: error if wrt is the index
22
+ if @term.diff(wrt) != 0
23
+ Sum.new(@term.diff(wrt),@index,@lb,@ub)
24
+ else
25
+ 0
26
+ end
27
+ end
28
+ def variables
29
+ @term.variables.reject{|var| var == @index} #@index doesn't really count
30
+ end
31
+ def value
32
+ self.expand.value
33
+ end
34
+ def subs(to_replace, replacement=nil)
35
+ #TODO: error if to_replace is @index
36
+ if replacement == nil and to_replace.is_a?(Hash)
37
+ super(to_replace)
38
+ else
39
+ Sum.new(@term.subs(to_replace, replacement),@index,@lb,@ub)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,24 +1,20 @@
1
1
  # TODO: 2*symbolic is a 2 power of symbolic Summand
2
+ require "#{File.dirname(__FILE__)}/expression.rb"
2
3
  module Symbolic
4
+ require "#{File.dirname(__FILE__)}/expression.rb"
3
5
  class Summands < Expression
6
+ OPERATION = :+
7
+ IDENTITY = 0
4
8
  class << self
5
- def operation
6
- '+'
7
- end
8
-
9
- def identity_element
10
- 0
11
- end
12
-
13
9
  def summands(summands)
14
10
  summands
15
11
  end
16
12
 
17
13
  def factors(factors)
18
- if factors.symbolic.length == 1 && factors.symbolic.values.first == 1
19
- new identity_element, factors.symbolic.keys.first => factors.numeric
14
+ if factors.symbolic.length == 1 && factors.symbolic.first[1] == Factors::IDENTITY
15
+ new IDENTITY, factors.symbolic.first[0] => factors.numeric
20
16
  else
21
- new identity_element, Factors.new(1, factors.symbolic) => factors.numeric
17
+ new IDENTITY, Factors.new(1, factors.symbolic) => factors.numeric
22
18
  end
23
19
  end
24
20
 
@@ -27,41 +23,50 @@ module Symbolic
27
23
  end
28
24
 
29
25
  def simplify(numeric, symbolic)
30
- if symbolic.empty?
26
+ if symbolic.empty? #only the numeric portion
31
27
  numeric
32
- elsif numeric == identity_element && symbolic.size == 1
33
- symbolic.values.first * symbolic.keys.first
28
+ elsif numeric == IDENTITY && symbolic.size == 1 #no numeric to add and only one symbolic, so can just return the one base*coefficient
29
+ symbolic.first[1] * symbolic.first[0]
30
+ elsif symbolic.size > 1 #let's look to see if any base gets repeated, so that they can be combined
31
+ temp = []
32
+ symbolic.each_key do |base1|
33
+ temp = symbolic.find_all{|base2,v2| base1 == base2} #temp is an array of form [[b,c1],[b,c2]...]
34
+ break if temp.size > 1 #found a duplicate base
35
+ end
36
+ if temp.size > 1
37
+ repeated_base = temp[0][0]
38
+ new_coef = temp.inject(0){|sum, (b,coeff)| sum + coeff} #sum up the old coefficients
39
+ #it could be that there is more than one repeated base, but the next line effectively is recursion, and it'll take care of that
40
+ Summands.new(numeric, symbolic.reject{|k,v| k == repeated_base}) + new_coef * repeated_base
41
+ else
42
+ nil
43
+ end
34
44
  end
35
45
  end
36
46
  end
37
47
 
38
48
  def value
39
- if variables.all? &:value
49
+ if variables.all?(&:value)
40
50
  symbolic.inject(numeric) {|value, (base, coef)| value + base.value * coef.value }
41
51
  end
42
52
  end
43
53
 
44
- def to_s
45
- output = symbolic.map {|base, coef| coef_to_string(coef) + base.to_s }
46
- output << remainder_to_string(numeric) if numeric != 0
47
- output[0].sub!(/^\+/, '')
48
- output.join
49
- end
50
-
51
54
  def reverse
52
- self.class.new -numeric, Hash[*symbolic.map {|k,v| [k,-v]}.flatten]
55
+ self.class.new( -numeric, Hash[*symbolic.map {|k,v| [k,-v]}.flatten] )
53
56
  end
54
-
55
- def coef_to_string(coef)
56
- "#{(coef > 0) ? '+' : '-' }#{ "#{rational_to_string(coef.abs)}*" if coef.abs != 1}"
57
+ def value
58
+ @symbolic.inject(@numeric){|m,(base,coefficient)| m + coefficient * base.value}
57
59
  end
58
-
59
- def remainder_to_string(numeric)
60
- "#{'+' if numeric > 0}#{numeric}"
60
+ def subs(to_replace, replacement=nil)
61
+ if replacement == nil and to_replace.is_a?(Hash)
62
+ super(to_replace)
63
+ else
64
+ @symbolic.inject(@numeric){|m,(base,coefficient)| m + coefficient * base.subs(to_replace, replacement)}
65
+ end
61
66
  end
62
67
 
63
- def rational_to_string(numeric)
64
- ((numeric.round == numeric) ? numeric.to_i : numeric.to_f).to_s
68
+ def diff(wrt)
69
+ @symbolic.inject(0){|m,(base,coefficient)| m + coefficient * base.diff(wrt)}
65
70
  end
66
71
  end
67
72
  end
@@ -1,32 +1,44 @@
1
1
  module Symbolic
2
2
  =begin
3
- This class is used to create symbolic variables.
4
- Symbolic variables presented by name and value.
5
- Name is neccessary for printing meaningful symbolic expressions.
6
- Value is neccesary for calculation of symbolic expressions.
7
- If value isn't set for variable, but there is an associated proc, then value is taken from evaluating the proc.
3
+ This class is used to create symbolic variables.
4
+ Symbolic variables presented by name and value.
5
+ Name is neccessary for printing meaningful symbolic expressions.
6
+ Value is neccesary for calculation of symbolic expressions.
7
+ If value isn't set for variable, but there is an associated proc, then value is taken from evaluating the proc.
8
8
  =end
9
9
  class Variable
10
10
  include Symbolic
11
11
  attr_accessor :name, :proc
12
12
  attr_writer :value
13
13
 
14
+ # Create a new Symbolic::Variable, with optional name, value and proc
14
15
  def initialize(options={}, &proc)
15
- @name, @value = options.values_at(:name, :value)
16
+ (@name, @value), @proc = options.values_at(:name, :value), proc
16
17
  @name = @name.to_s if @name
17
- @proc = proc
18
18
  end
19
19
 
20
20
  def value
21
21
  @value || @proc && @proc.call.value
22
22
  end
23
23
 
24
- def to_s
25
- @name || 'unnamed_variable'
26
- end
27
-
28
24
  def variables
29
25
  [self]
30
26
  end
27
+
28
+ def subs(to_replace, replacement=nil, expect_numeric = false)
29
+ if replacement == nil and to_replace.is_a?(Hash)
30
+ super(to_replace)
31
+ else
32
+ return replacement if self == to_replace
33
+ #Consider the possibility that @value is not numeric?
34
+ return self.value if expect_numeric
35
+ self
36
+ end
37
+ end
38
+
39
+ def diff(wrt)
40
+ return 1 if self == wrt
41
+ 0
42
+ end
31
43
  end
32
44
  end
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env ruby -w
2
+ require File.expand_path('../spec_helper', __FILE__)
3
+
4
+ describe Symbolic do
5
+ describe "evaluation (x=1, y=2):" do
6
+ x = var :name => :x, :value => 1
7
+ y = var :name => :y, :value => 2
8
+ {
9
+ x => 1,
10
+ y => 2,
11
+ +x => 1,
12
+ -x => -1,
13
+ x + 4 => 5,
14
+ 3 + x => 4,
15
+ x + y => 3,
16
+ x - 1 => 0,
17
+ 1 - x => 0,
18
+ x - y => -1,
19
+ -x + 3 => 2,
20
+ -y - x => -3,
21
+ x*3 => 3,
22
+ 4*y => 8,
23
+ (+x)*(-y) => -2,
24
+ x/2 => 0.5,
25
+ y/2 => 1,
26
+ -2/x => -2,
27
+ 4/(-y) => -2,
28
+ x**2 => 1,
29
+ 4**y => 16,
30
+ y**x => 2,
31
+ x-(y+x)/5 => Rational(2,5),
32
+ }.each_pair { |expr, value|
33
+ it expr do
34
+ expr.value.should == value
35
+ end
36
+ }
37
+ end
38
+
39
+ describe "optimization:" do
40
+ x = var :name => :x
41
+ y = var :name => :y
42
+ {
43
+ -(-x) => x,
44
+
45
+ 0 + x => x,
46
+ x + 0 => x,
47
+ x + (-2) => x - 2,
48
+ -2 + x => x - 2,
49
+ -x + 2 => 2 - x,
50
+ x + (-y) => x - y,
51
+ -y + x => x - y,
52
+
53
+ 0 - x => -x,
54
+ x - 0 => x,
55
+ x - (-2) => x + 2,
56
+ -2 - (-x) => x - 2,
57
+ x - (-y) => x + y,
58
+
59
+ 0 * x => 0,
60
+ x * 0 => 0,
61
+ 1 * x => x,
62
+ x * 1 => x,
63
+ -1 * x => -x,
64
+ x * (-1) => -x,
65
+ x * (-3) => -(x*3),
66
+ -3 * x => -(x*3),
67
+ -3 * (-x) => x*3,
68
+ x*(-y) => -(x*y),
69
+ -x*y => -(x*y),
70
+ (-x)*(-y) => x*y,
71
+
72
+ 0 / x => 0,
73
+ x / 1 => x,
74
+
75
+ 0**x => 0,
76
+ 1**x => 1,
77
+ x**0 => 1,
78
+ x**1 => x,
79
+ (-x)**1 => -x,
80
+ (-x)**2 => x**2,
81
+ (x**2)**y => x**(2*y),
82
+
83
+ x*4*x => 4*x**2,
84
+ x*(-1)*x**(-1) => -1,
85
+ x**2*(-1)*x**(-1) => -x,
86
+ x + y - x => y,
87
+ 2*x + x**1 - y**2/y - y => 3*x - 2*y,
88
+ -(x+4) => -x-4,
89
+
90
+ (x/y)/(x/y) => 1,
91
+ (y/x)/(x/y) => y**2/x**2,
92
+
93
+ x - (y+x)/5 => 0.8*x-0.2*y,
94
+ }.each_pair { |non_optimized, optimized|
95
+ it non_optimized do
96
+ non_optimized.should == optimized
97
+ end
98
+ }
99
+ end
100
+
101
+ describe 'Variable methods:' do
102
+ let(:v) { var :name => :v }
103
+ let(:x) { var :name => :x, :value => 2 }
104
+ let(:y) { var :name => :y }
105
+
106
+ #initialize
107
+ it 'var(:name => :v)' do
108
+ v.name.should == 'v'
109
+ v.value.should be nil
110
+ end
111
+ it 'var.to_s == "unnamed_variable"' do
112
+ v.name = nil
113
+ v.to_s.should == 'unnamed_variable'
114
+ v.value.should be nil
115
+ end
116
+ it 'var init' do
117
+ x.value.should == 2
118
+ end
119
+ it 'proc value' do
120
+ y = var { x**2 }
121
+ x.value = 3
122
+ (x*y).value.should == 27
123
+ end
124
+
125
+ it 'expression variables' do
126
+ x.variables.should == [x]
127
+ (-(x+y)).variables.should == [x,y]
128
+ end
129
+
130
+ it 'operations' do
131
+ (-x**y-4*y+5-y/x).operations.should == {:+ => 1, :- => 2, :* => 1, :/ => 1, :-@ => 1, :** => 1}
132
+ end
133
+
134
+ it 'math method' do
135
+ cos = Symbolic::Math.cos(x)
136
+ x.value = 0
137
+ cos.value.should == 1.0
138
+ cos.to_s.should == 'cos(x)'
139
+ end
140
+ end
141
+
142
+ describe "to_s:" do
143
+ x = var :name => :x
144
+ y = var :name => :y
145
+ {
146
+ x => 'x',
147
+ -x => '-x',
148
+ x+1 => 'x+1',
149
+ x-4 => 'x-4',
150
+ -x-4 => '-x-4',
151
+ -(x+y) => '-x-y',
152
+ -(x-y) => '-x+y',
153
+ x*y => 'x*y',
154
+ (-x)*y => '-x*y',
155
+ (y+3)*(x+2)*4 => '4*(y+3)*(x+2)',
156
+ 4/x => '4/x',
157
+ 2*x**(-1)*y**(-1) => '2/(x*y)',
158
+ (-(2+x))/(-(-y)) => '(-x-2)/y',
159
+ x**y => 'x**y',
160
+ x**(y-4) => 'x**(y-4)',
161
+ (x+1)**(y*2) => '(x+1)**(2*y)',
162
+ -(x**y-2)+5 => '-x**y+7',
163
+ x-(y+x)/5 => 'x-0.2*(y+x)',
164
+ }.each_pair { |expr, str|
165
+ it str do
166
+ expr.to_s.should == str
167
+ end
168
+ }
169
+ end
170
+ end
171
+
172
+ describe "Symbolic plugins" do
173
+ if RUBY_VERSION > '1.9' # Not yet compatible with 1.8
174
+ describe "var_name" do
175
+ require "symbolic/plugins/var_name"
176
+ it 'single variable' do
177
+ x = var
178
+ x.name.should == 'x'
179
+ end
180
+
181
+ it 'single variable with value' do
182
+ x = var :value => 7
183
+ x.name.should == 'x'
184
+ x.value.should == 7
185
+ end
186
+
187
+ it 'multiple variables' do
188
+ x, yy, zzz = vars
189
+ [x, yy, zzz].map(&:name).should == ['x', 'yy', 'zzz']
190
+ end
191
+ end
192
+ end
193
+ end