symbolic 0.3.7 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,6 +2,8 @@ Symbolic math for ruby.
2
2
 
3
3
  == Installation
4
4
 
5
+ Symbolic needs Ruby 1.9.
6
+
5
7
  gem install symbolic
6
8
 
7
9
  == Introduction
@@ -11,7 +13,7 @@ This gem can help you
11
13
  - if you want to speed up similar calculations
12
14
  - if you need an abstraction layer for math
13
15
 
14
- Symbolic doesn't have any external dependencies. It uses only pure ruby (less than 400 lines of code).
16
+ Symbolic doesn't have any external dependencies.
15
17
 
16
18
  == Tutorial
17
19
 
@@ -40,6 +42,14 @@ To get value of symbolic expression you just call value:
40
42
 
41
43
  f.value # => 7
42
44
 
45
+ You can accomplish the same thing with subs:
46
+
47
+ f.subs(x,3) # => 7
48
+
49
+ Or make a more complicated substitution:
50
+
51
+ f.subs(x,x**2) # => 2*x**2+1
52
+
43
53
  If symbolic expression contains variables without value then it returns nil.
44
54
 
45
55
  z = var
@@ -67,16 +77,22 @@ So you can get a list of variables without value:
67
77
 
68
78
  (x+y+1).variables.select {|var| var.value.nil? }
69
79
 
70
- You can get an information about number of different operations used in a symbolic expression:
80
+ You can get information about the number of different operations used in a symbolic expression:
71
81
 
72
82
  f = (2*x-y+2)*x-2**(x*y)
73
83
  f.operations # => {"+"=>1, "-"=>2, "*"=>3, "/"=>0, "**"=>1, "-@"=>0}
74
84
 
85
+ You can also take derivitives and do taylor expansions:
86
+
87
+ Symbolic::Math.cos(x**2).diff(x)
88
+ # => -2*(sin(x**2))*x
89
+ Symbolic::Math.cos(x).taylor(x,0,3)
90
+ # => -0.5*x**2+1.0
91
+
75
92
 
76
93
  == TODO
77
94
  - a lot of refactoring (code is pretty messy at this stage)
78
95
  - plotting capabilities
79
- - derivatives
80
96
  - integrals
81
97
  - thorough documentation
82
98
 
@@ -85,4 +101,4 @@ You can get an information about number of different operations used in a symbol
85
101
  brainopia (ravwar at gmail.com).
86
102
 
87
103
  I am ready to help with any questions related to Symbolic.
88
- I welcome any contribution.
104
+ I welcome any contribution.
data/Rakefile CHANGED
@@ -5,13 +5,13 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "symbolic"
8
- gem.version = '0.3.7'
8
+ gem.version = '0.3.8'
9
9
  gem.summary = 'Symbolic math for ruby'
10
10
  gem.description = 'Symbolic math for ruby. This gem can help if you want to get a simplified form of a big equation or to speed up similar calculations or you need an abstraction layer for math. Symbolic does not have any external dependencies. It uses only pure ruby (less than 400 lines of code).'
11
11
  gem.email = "ravwar@gmail.com"
12
12
  gem.homepage = "http://brainopia.github.com/symbolic"
13
13
  gem.authors = ["brainopia"]
14
- gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_development_dependency "rspec", ">= 2.4"
15
15
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
16
  end
17
17
  Jeweler::GemcutterTasks.new
@@ -19,18 +19,7 @@ rescue LoadError
19
19
  puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
20
  end
21
21
 
22
- require 'spec/rake/spectask'
23
- Spec::Rake::SpecTask.new(:spec) do |spec|
24
- spec.libs << 'lib' << 'spec'
25
- spec.spec_files = FileList['spec/**/*_spec.rb']
26
- end
27
-
28
- Spec::Rake::SpecTask.new(:rcov) do |spec|
29
- spec.libs << 'lib' << 'spec'
30
- spec.pattern = 'spec/**/*_spec.rb'
31
- spec.rcov = true
32
- end
33
-
34
- task :spec => :check_dependencies
22
+ require 'rspec/core/rake_task'
23
+ RSpec::Core::RakeTask.new(:spec)
35
24
 
36
- task :default => :spec
25
+ task :default => :spec
@@ -1,17 +1,3 @@
1
- require 'symbolic/coerced'
2
- require 'symbolic/variable'
3
- require 'symbolic/expression'
4
- require 'symbolic/summands'
5
- require 'symbolic/factors'
6
- require 'symbolic/function'
7
- require 'symbolic/math'
8
- require 'symbolic/statistics'
9
-
10
- require 'symbolic/extensions/kernel'
11
- require 'symbolic/extensions/numeric'
12
- require 'symbolic/extensions/matrix' if Object.const_defined? 'Matrix'
13
- require 'symbolic/extensions/rational' if RUBY_VERSION == '1.8.7'
14
-
15
1
  module Symbolic
16
2
  def +@
17
3
  self
@@ -45,13 +31,38 @@ module Symbolic
45
31
  [Coerced.new(self), numeric]
46
32
  end
47
33
 
34
+ def to_s
35
+ Printer.print(self)
36
+ end
37
+
48
38
  def inspect
49
39
  "Symbolic(#{to_s})"
50
40
  end
51
41
 
52
- private
53
-
54
- def variables_of(var)
55
- var.variables rescue []
42
+ def taylor(var, about, numterms=5)
43
+ term = self
44
+ #inject needs initial value to prevent it from eating the first term
45
+ (0..numterms-1).inject(0) do |sum,n|
46
+ to_add = term.subs(var,about) * (var - about) ** n / factorial(n)
47
+ term = term.diff(var) #save a little time by not having to do all the derivites every time
48
+ sum + to_add
49
+ end
50
+ end
51
+
52
+ #make multiple substitutions using a hash. Ex: (x+y+z).subs({x=>2*y,z=>y**2}) results in y**2+3*y
53
+ def subs(hsh)
54
+ temp = self
55
+ hsh.each{|k,v| temp = temp.subs(k,v)}
56
+ temp
56
57
  end
57
- end
58
+
59
+ end
60
+
61
+ #in order they should be loaded
62
+ ['symbolic/expression.rb','symbolic/coerced.rb','symbolic/constants.rb','symbolic/factors.rb',
63
+ 'symbolic/printer.rb','symbolic/sum.rb','symbolic/variable.rb','symbolic/constant.rb',
64
+ 'symbolic/function.rb','symbolic/misc.rb','symbolic/statistics.rb',
65
+ 'symbolic/summands.rb','symbolic/extensions/kernel.rb','symbolic/extensions/matrix.rb','symbolic/extensions/module.rb',
66
+ 'symbolic/extensions/numeric.rb','symbolic/extensions/rational.rb','symbolic/math.rb'].each do |file|
67
+ require File.dirname(__FILE__) + '/' + file
68
+ end
@@ -0,0 +1,27 @@
1
+ module Symbolic
2
+ class Constant
3
+ include Symbolic
4
+ attr_reader :name, :value
5
+
6
+ # Create a new Symbolic::Variable, with optional name, value and proc
7
+ def initialize(value , name = nil)
8
+ @name, @value = name, value
9
+ @name = @name.to_s if @name
10
+ end
11
+ def subs(to_replace, replacement=nil)
12
+ if replacement == nil and to_replace.is_a?(Hash)
13
+ super(to_replace)
14
+ else
15
+ return replacement if self == to_replace
16
+ self
17
+ end
18
+ end
19
+ def diff(wrt)
20
+ 0
21
+ end
22
+ #yeah, it's not a variable, but it acts like one as far as value is concerned
23
+ def variables
24
+ [self]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'complex'
4
+ module Symbolic::Constants
5
+ require "#{File.dirname(__FILE__)}/constant.rb"
6
+ if RUBY_VERSION < '1.9'
7
+ PI = Symbolic::Constant.new(::Math::PI,'PI')
8
+ I = Symbolic::Constant.new(::Complex::I,'i')
9
+ E = Symbolic::Constant.new(::Math::E,'e')
10
+ else #we can use unicode
11
+ PI = Symbolic::Constant.new(::Math::PI,'π')
12
+ I = Symbolic::Constant.new(::Complex::I,'ⅈ')
13
+ E = Symbolic::Constant.new(::Math::E,'ⅇ')
14
+ end
15
+ end
@@ -23,7 +23,7 @@ module Symbolic
23
23
  end
24
24
 
25
25
  def unite_numeric(numeric1, numeric2)
26
- numeric1.send operation, numeric2
26
+ numeric1.send self::OPERATION, numeric2
27
27
  end
28
28
 
29
29
  def convert(var)
@@ -39,7 +39,7 @@ module Symbolic
39
39
  end
40
40
 
41
41
  def one(symbolic)
42
- new identity_element, symbolic => 1
42
+ new self::IDENTITY, symbolic => 1
43
43
  end
44
44
 
45
45
  def simple?(var)
@@ -63,11 +63,25 @@ module Symbolic
63
63
  end
64
64
 
65
65
  def variables
66
- @symbolic.map {|k,v| [variables_of(k), variables_of(v)] }.flatten.uniq
66
+ @symbolic.map {|k,v| [k.variables, v.variables] }.flatten.uniq
67
67
  end
68
68
 
69
69
  def ==(object)
70
- (object.numeric == @numeric) and (object.symbolic == @symbolic) rescue false
70
+ # We need to make sure the classes are the same because both Factors and
71
+ # Summands have .numeric and .symbolic, but we can't say they're equal
72
+ object.class == self.class and
73
+ object.numeric == @numeric and
74
+ # Make sure that we have the same number of elements, otherwise the
75
+ # next step could give false positives
76
+ object.symbolic.size == @symbolic.size and
77
+ # hash's == function only checks that the object_ids are equal, but we
78
+ # could have different instances of the same object (mathematically speaking). We
79
+ # need to check that each thing in @symbolic appears in object.symbolic as well.
80
+ object.symbolic.inject(true) do |memo,(key,value)| # go through each kv pair in object.symbolic
81
+ memo and @symbolic.inject(false) do |memo2,(key2,value2)|# and make sure it appears in @symbolic
82
+ memo2 or (key2 == key and value2 == value)
83
+ end
84
+ end
71
85
  end
72
86
  end
73
87
  end
@@ -1,5 +1,5 @@
1
1
  module Kernel
2
2
  def var(options={}, &proc)
3
- Symbolic::Variable.new options, &proc
3
+ Symbolic::Variable.new(options, &proc)
4
4
  end
5
5
  end
@@ -1,9 +1,11 @@
1
- class Matrix
2
- def value
3
- map {|it| it.value }
4
- end
1
+ if Object.const_defined? 'Matrix'
2
+ class Matrix
3
+ def value
4
+ map {|it| it.value }
5
+ end
5
6
 
6
- def variables
7
- map {|it| it.variables }.to_a.flatten.uniq
7
+ def variables
8
+ map {|it| it.variables }.to_a.flatten.uniq
9
+ end
8
10
  end
9
11
  end
@@ -0,0 +1,5 @@
1
+ class Module
2
+ def simple_name
3
+ name.split('::').last
4
+ end
5
+ end
@@ -2,4 +2,12 @@ class Numeric
2
2
  def value
3
3
  self
4
4
  end
5
+
6
+ def variables
7
+ []
8
+ end
9
+
10
+ def subs(to_replace, replacement)
11
+ self
12
+ end
5
13
  end
@@ -1,13 +1,19 @@
1
- # Fix for rational on 1.8.7
2
- [Fixnum,Bignum].each do |klass|
3
- klass.class_eval do
4
- def rpower(other)
5
- if other.is_a?(Numeric) && other < 0
6
- Rational.new!(self, 1)**other
7
- else
8
- self.power!(other)
1
+ # Fix for Integer#** defined in stdlib rational.rb on ruby < 1.9
2
+ # We need to redefine Integer#**(other)
3
+ # Because, with Rational, it uses a comparaison such as other >= 0
4
+ # That cannot work with Symbolic::Variable (or make sense)
5
+ if RUBY_VERSION < '1.9'
6
+ require 'rational'
7
+ [Fixnum,Bignum].each do |klass|
8
+ klass.class_eval do
9
+ alias :old_power :**
10
+ def **(other)
11
+ if Numeric === other # If other is Numeric, we can use rpower(the new #** defined in stdlib rational.rb)
12
+ old_power(other)
13
+ else # But if not, we want the old behaviour(that was aliased to power!, and does not check if >= 0)
14
+ power!(other)
15
+ end
9
16
  end
10
- end
11
- alias ** rpower
12
- end
13
- end
17
+ end # class_eval
18
+ end # each
19
+ end # if
@@ -1,14 +1,8 @@
1
1
  module Symbolic
2
2
  class Factors < Expression
3
+ OPERATION = :*
4
+ IDENTITY = 1
3
5
  class << self
4
- def operation
5
- '*'
6
- end
7
-
8
- def identity_element
9
- 1
10
- end
11
-
12
6
  def summands(summands)
13
7
  one summands
14
8
  end
@@ -48,23 +42,23 @@ module Symbolic
48
42
  end
49
43
 
50
44
  def simplify_expression!(factors)
51
- factors[1].delete_if {|base, exp| (base == identity_element) || (exp == 0) }
52
- factors[0] = 0 if factors[1].any? {|base, exp| base == 0 }
45
+ factors[1].delete_if {|base, exp| (base == IDENTITY) || (exp == 0) }
46
+ factors[0] = 0 if factors[1].any? {|base, _| base == 0 }
53
47
  end
54
48
 
55
49
  def simplify(numeric, symbolic)
56
50
  if numeric == 0 || symbolic.empty?
57
51
  (numeric.round == numeric) ? numeric.to_i : numeric.to_f
58
- elsif numeric == identity_element && symbolic.size == 1 && symbolic.values.first == 1
59
- symbolic.keys.first
52
+ elsif numeric == IDENTITY && symbolic.size == 1 && symbolic.first[1] == 1
53
+ symbolic.first[0]
60
54
  end
61
55
  end
62
56
 
63
57
  def unite_exponents(base, exponent)
64
58
  if base.is_a? Factors
65
- return base.numeric**exponent, Hash[*base.symbolic.map {|base,exp| [base,exp*exponent] }.flatten]
59
+ [base.numeric**exponent, Hash[*base.symbolic.map {|b,e| [b, e*exponent] }.flatten]]
66
60
  else
67
- [identity_element, { base => exponent }]
61
+ [IDENTITY, { base => exponent }]
68
62
  end
69
63
  end
70
64
  end
@@ -74,48 +68,32 @@ module Symbolic
74
68
  end
75
69
 
76
70
  def value
77
- if variables.all? &:value
71
+ if variables.all?(&:value)
78
72
  @symbolic.inject(numeric) {|value, (base, exp)| value * base.value ** exp.value }
79
73
  end
80
74
  end
81
75
 
82
- def to_s
83
- simplify_output
84
- end
85
-
86
- def coefficient_to_string(numeric)
87
- "#{'-' if numeric < 0}#{"#{rational_to_string numeric.abs}*" if numeric.abs != 1}"
88
- end
89
-
90
- def exponent_to_string(base, exponent)
91
- "#{brackets base}#{"**#{brackets exponent}" if exponent != 1}"
92
- end
93
-
94
- def brackets(var)
95
- [Numeric, Variable, Function].any? {|klass| var.is_a? klass } ? var : "(#{var})"
96
- end
97
-
98
- def simplify_output
99
- groups = @symbolic.group_by {|b,e| e.is_a?(Numeric) && e < 0 }
100
- reversed_factors = groups[true] ? [1, Hash[*groups[true].flatten] ] : nil
101
- factors = groups[false] ? [@numeric, Hash[*groups[false].flatten] ] : nil
102
- output = '' << (factors ? output(factors) : rational_to_string(@numeric))
103
- output << "/#{reversed_output reversed_factors}" if reversed_factors
104
- output
105
- end
106
-
107
- def output(factors)
108
- coefficient_to_string(factors[0]) <<
109
- factors[1].map {|base,exp| exponent_to_string base,exp }.join('*')
76
+ def subs(to_replace, replacement=nil)
77
+ if replacement == nil and to_replace.is_a?(Hash)
78
+ super(to_replace)
79
+ else
80
+ @symbolic.inject(@numeric){|m,(base,exponential)| m * base.subs(to_replace, replacement) ** exponential.subs(to_replace, replacement)}
81
+ end
110
82
  end
111
83
 
112
- def reversed_output(factors)
113
- result = output [factors[0], Hash[*factors[1].map {|b,e| [b,-e] }.flatten]]
114
- (factors[1].length > 1) ? "(#{result})" : result
84
+ def value
85
+ @symbolic.inject(@numeric){|m,(base,exponential)| m * base.value ** exponential.value}
115
86
  end
116
87
 
117
- def rational_to_string(numeric)
118
- ((numeric.round == numeric) ? numeric.to_i : numeric.to_f).to_s
88
+ def diff(wrt)
89
+ return 0 unless self.variables.include?(wrt) #speed things up a bit
90
+ first_base, first_exp = @symbolic.to_a[0]
91
+ first = first_base ** first_exp #the first factor
92
+ self_without_first = self / first #the expression with the first factor removed
93
+ #product rule to find derivitive
94
+ udv = self_without_first.is_a?(Symbolic) ? first * self_without_first.diff(wrt) : 0
95
+ vdu = first_base.is_a?(Symbolic) ? first_exp * first_base ** (first_exp - 1 ) * first_base.diff(wrt) * self_without_first : 0
96
+ udv + vdu
119
97
  end
120
98
  end
121
99
  end
@@ -1,26 +1,107 @@
1
- class Symbolic::Function
2
- =begin
3
- This class is proxy for methods from Math module.
4
- =end
5
- include Symbolic
6
-
7
- def initialize(argument, operation)
8
- @argument, @operation = argument, operation
9
- end
1
+ module Symbolic
2
+ class Function
3
+ attr_reader :name, :deriv, :operation
10
4
 
11
- def to_s
12
- "#{@operation}(#{@argument})"
13
- end
5
+ #deriv can be either a function or a proc that takes an argument -- it can be set later
6
+ #operation can be passed as a block, a proc, or set later
7
+ def initialize(name, deriv = nil, op = nil, &block)
8
+ @name, @deriv = name, deriv
9
+ unless op == nil
10
+ @operation = op
11
+ else
12
+ @operation = block
13
+ end
14
+ end
14
15
 
15
- def value
16
- ::Math.send @operation, @argument.value if variables.all? &:value
17
- end
16
+ #it may be easier to set the derivitve after the object is made
17
+ def set_derivative(deriv)
18
+ #only allow it to be set if @derivative is nil
19
+ @deriv = deriv if @deriv == nil
20
+ end
21
+
22
+ def set_operation(op)
23
+ #only allow it to be set if @derivative is nil
24
+ @operation = op if @operation == nil
25
+ end
18
26
 
19
- def variables
20
- @argument.variables
27
+ #returns a FunctionWrapper with the argument or a number
28
+ def [] (arg)
29
+ if arg.is_a?(::Numeric) #take care of the case where arg is a number
30
+ self.call(arg)
31
+ else
32
+ FunctionWrapper.new(arg, self)
33
+ end
34
+ end
35
+
36
+ #same but with different syntax
37
+ def arg(arg)
38
+ self[arg]
39
+ end
40
+
41
+ #returns the derivitve with arg plugged in -- for use with chainrule
42
+ def derivative(arg)
43
+ if @deriv.is_a?(Proc)
44
+ @deriv.call(arg)
45
+ elsif @deriv.is_a?(Function)
46
+ @deriv[arg]
47
+ else #by process of elimination, it's a Symbolic
48
+ @deriv.subs(Symbolic::Math::Arg,arg)
49
+ end
50
+ end
51
+
52
+ def call(arg)
53
+ @operation.call(arg)
54
+ end
21
55
  end
22
56
 
23
- def detailed_operations
24
- @argument.detailed_operations.tap {|it| it[@operation] += 1}
57
+ #class combines a function with an argument or arguments
58
+ #this class is what allows functions to be used in symbolic expressions
59
+ class FunctionWrapper
60
+ include Symbolic
61
+ attr_reader :argument, :function
62
+
63
+ def initialize(arg, fctn)
64
+ @argument, @function = arg, fctn
65
+ end
66
+
67
+ def name
68
+ @function.name
69
+ end
70
+
71
+ def value
72
+ @function.call(@argument.value)
73
+ end
74
+
75
+ def variables
76
+ @argument.variables
77
+ end
78
+
79
+ def detailed_operations
80
+ @argument.detailed_operations.tap {|it| it[@operation] += 1}
81
+ end
82
+
83
+ def subs(to_replace, replacement=nil)
84
+ if replacement == nil and to_replace.is_a?(Hash)
85
+ super(to_replace)
86
+ else
87
+ @function[@argument.subs(to_replace, replacement)]
88
+ end
89
+ end
90
+
91
+ #simply dumps @argument in to the function -- no gaurentee that the function
92
+ #will know how to handle it. Useful in some circumstances
93
+ def eval
94
+ @function.call(@argument)
95
+ end
96
+
97
+ def ==(object)
98
+ (object.function == @function and object.argument == @argument) rescue false
99
+ end
100
+
101
+ def diff(wrt)
102
+ return 0 unless self.variables.member?(wrt)
103
+ #chain rule
104
+ @function.derivative(@argument) * @argument.diff(wrt)
105
+ end
25
106
  end
26
107
  end