├── .gitignore ├── README.rdoc ├── Rakefile ├── lib ├── symbolic.rb └── symbolic │ ├── coerced.rb │ ├── constant.rb │ ├── constants.rb │ ├── expression.rb │ ├── extensions │ ├── kernel.rb │ ├── matrix.rb │ ├── module.rb │ ├── numeric.rb │ └── rational.rb │ ├── factors.rb │ ├── function.rb │ ├── math.rb │ ├── misc.rb │ ├── plugins │ └── var_name.rb │ ├── printer.rb │ ├── statistics.rb │ ├── sum.rb │ ├── summands.rb │ └── variable.rb ├── spec ├── integration_spec.rb ├── spec_helper.rb ├── symbolic │ └── equal_spec.rb └── symbolic_spec.rb └── symbolic.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | rdoc 3 | pkg 4 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | Symbolic math for ruby. 2 | 3 | == Installation 4 | 5 | Symbolic needs Ruby 1.9. 6 | 7 | gem install symbolic 8 | 9 | == Introduction 10 | 11 | This gem can help you 12 | - if you want to get a simplified form of a big equation 13 | - if you want to speed up similar calculations 14 | - if you need an abstraction layer for math 15 | 16 | Symbolic doesn't have any external dependencies. 17 | 18 | == Tutorial 19 | 20 | First, you need to create a symbolic variable. 21 | 22 | x = var 23 | 24 | # you can set a name of variable (useful if you print equations) 25 | angle = var :name => 'θ' 26 | 27 | # or starting value 28 | pi = var :value => 3.14 29 | 30 | # or bind its value to a proc 31 | y = var { x ** 2 } 32 | 33 | # you can set a value for already created variable 34 | x.value = 3 35 | 36 | Now, you can do any math operations with it. 37 | 38 | f = 2*x + 1 39 | puts f # => 2*x+1 40 | 41 | To get value of symbolic expression you just call value: 42 | 43 | f.value # => 7 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 | 53 | If symbolic expression contains variables without value then it returns nil. 54 | 55 | z = var 56 | (z+1).value # => nil 57 | 58 | All symbolic expression are automatically simplified when created: 59 | 60 | 0 * x # => 0 61 | 2 + x + 1 # => x + 3 62 | -(x-y) + 2*x # => x + y 63 | (x**2)**3 / x # => x**5 64 | # etc. (more examples can be found in symbolic_spec.rb) 65 | 66 | If you need to use a function from Math module with symbolic variable, use Symbolic::Math module. 67 | 68 | cos = Symbolic::Math.cos(x) 69 | x.value = 0 70 | cos.value # => 1.0 71 | 72 | You can get a list of variables from symbolic expression: 73 | 74 | (x+y+1).variables # => [x, y] 75 | 76 | So you can get a list of variables without value: 77 | 78 | (x+y+1).variables.select {|var| var.value.nil? } 79 | 80 | You can get information about the number of different operations used in a symbolic expression: 81 | 82 | f = (2*x-y+2)*x-2**(x*y) 83 | f.operations # => {"+"=>1, "-"=>2, "*"=>3, "/"=>0, "**"=>1, "-@"=>0} 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 | 92 | 93 | == TODO 94 | - a lot of refactoring (code is pretty messy at this stage) 95 | - plotting capabilities 96 | - integrals 97 | - thorough documentation 98 | 99 | == Author 100 | 101 | brainopia (ravwar at gmail.com). 102 | 103 | I am ready to help with any questions related to Symbolic. 104 | I welcome any contribution. 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "symbolic" 8 | gem.version = '0.3.7' 9 | gem.summary = 'Symbolic math for ruby' 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 | gem.email = "ravwar@gmail.com" 12 | gem.homepage = "http://brainopia.github.com/symbolic" 13 | gem.authors = ["brainopia"] 14 | gem.add_development_dependency "rspec", ">= 2.4" 15 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 16 | end 17 | Jeweler::GemcutterTasks.new 18 | rescue LoadError 19 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 20 | end 21 | 22 | require 'rspec/core/rake_task' 23 | RSpec::Core::RakeTask.new(:spec) 24 | 25 | task :default => :spec -------------------------------------------------------------------------------- /lib/symbolic.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | def +@ 3 | self 4 | end 5 | 6 | def -@ 7 | Factors.add self, -1 8 | end 9 | 10 | def +(var) 11 | Summands.add self, var 12 | end 13 | 14 | def -(var) 15 | Summands.subtract self, var 16 | end 17 | 18 | def *(var) 19 | Factors.add self, var 20 | end 21 | 22 | def /(var) 23 | Factors.subtract self, var 24 | end 25 | 26 | def **(var) 27 | Factors.power self, var 28 | end 29 | 30 | def coerce(numeric) 31 | [Coerced.new(self), numeric] 32 | end 33 | 34 | def to_s 35 | Printer.print(self) 36 | end 37 | 38 | def inspect 39 | "Symbolic(#{to_s})" 40 | end 41 | 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 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 69 | -------------------------------------------------------------------------------- /lib/symbolic/coerced.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | =begin 3 | This class restores a correct order of arguments after using a coerce method. 4 | For example, expression "1 - symbolic" calls to Symbolic#coerce 5 | which gives us a power to reverse a receiver and parameter of method 6 | so we set a receiver as Coerced.new(symbolic) to reverse arguments back after coercing. 7 | =end 8 | class Coerced 9 | def initialize(symbolic) 10 | @symbolic = symbolic 11 | end 12 | 13 | def +(numeric) 14 | Summands.add numeric, @symbolic 15 | end 16 | 17 | def -(numeric) 18 | Summands.subtract numeric, @symbolic 19 | end 20 | 21 | def *(numeric) 22 | Factors.add numeric, @symbolic 23 | end 24 | 25 | def /(numeric) 26 | Factors.subtract numeric, @symbolic 27 | end 28 | 29 | def **(numeric) 30 | Factors.power numeric, @symbolic 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/symbolic/constant.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/symbolic/constants.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/symbolic/expression.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | class Expression 3 | include Symbolic 4 | 5 | class << self 6 | def add(var1, var2) 7 | simplify_expression! expression = unite(convert(var1), convert(var2)) 8 | simplify(*expression) || new(*expression) 9 | end 10 | 11 | def subtract(var1, var2) 12 | add var1, convert(var2).reverse 13 | end 14 | 15 | def unite(expr1, expr2) 16 | numeric = unite_numeric expr1.numeric, expr2.numeric 17 | symbolic = unite_symbolic expr1.symbolic, expr2.symbolic 18 | [numeric, symbolic] 19 | end 20 | 21 | def unite_symbolic(symbolic1, symbolic2) 22 | symbolic1.merge(symbolic2) {|base, coef1, coef2| coef1 + coef2 } 23 | end 24 | 25 | def unite_numeric(numeric1, numeric2) 26 | numeric1.send self::OPERATION, numeric2 27 | end 28 | 29 | def convert(var) 30 | case var 31 | when Summands then summands var 32 | when Factors then factors var 33 | when Numeric then numeric var 34 | else one var; end 35 | end 36 | 37 | def numeric(numeric) 38 | new numeric, {} 39 | end 40 | 41 | def one(symbolic) 42 | new self::IDENTITY, symbolic => 1 43 | end 44 | 45 | def simple?(var) 46 | case var 47 | when Numeric, Variable, Function 48 | true 49 | when Summands 50 | false 51 | when Factors 52 | var.symbolic.all? {|k,v| simple? k } 53 | else 54 | false 55 | end 56 | end 57 | end 58 | 59 | attr_reader :numeric, :symbolic 60 | 61 | def initialize(numeric, symbolic) 62 | @numeric, @symbolic = numeric.freeze, symbolic.freeze 63 | end 64 | 65 | def variables 66 | @symbolic.map {|k,v| [k.variables, v.variables] }.flatten.uniq 67 | end 68 | 69 | def ==(object) 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 85 | end 86 | end 87 | end -------------------------------------------------------------------------------- /lib/symbolic/extensions/kernel.rb: -------------------------------------------------------------------------------- 1 | module Kernel 2 | def var(options={}, &proc) 3 | Symbolic::Variable.new(options, &proc) 4 | end 5 | end -------------------------------------------------------------------------------- /lib/symbolic/extensions/matrix.rb: -------------------------------------------------------------------------------- 1 | if Object.const_defined? 'Matrix' 2 | class Matrix 3 | def value 4 | map {|it| it.value } 5 | end 6 | 7 | def variables 8 | map {|it| it.variables }.to_a.flatten.uniq 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/symbolic/extensions/module.rb: -------------------------------------------------------------------------------- 1 | class Module 2 | def simple_name 3 | name.split('::').last 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/symbolic/extensions/numeric.rb: -------------------------------------------------------------------------------- 1 | class Numeric 2 | def value 3 | self 4 | end 5 | 6 | def variables 7 | [] 8 | end 9 | 10 | def subs(to_replace, replacement) 11 | self 12 | end 13 | end -------------------------------------------------------------------------------- /lib/symbolic/extensions/rational.rb: -------------------------------------------------------------------------------- 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 16 | end 17 | end # class_eval 18 | end # each 19 | end # if -------------------------------------------------------------------------------- /lib/symbolic/factors.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | class Factors < Expression 3 | OPERATION = :* 4 | IDENTITY = 1 5 | class << self 6 | def summands(summands) 7 | one summands 8 | end 9 | 10 | def factors(factors) 11 | factors 12 | end 13 | 14 | def power(base, exponent) 15 | simplify_expression! factors = unite_exponents(base, exponent) 16 | simplify(*factors) || new(*factors) 17 | end 18 | 19 | def add(var1, var2) 20 | if distributable? var1, var2 21 | distribute(var1, var2) 22 | elsif distributable? var2, var1 23 | distribute(var2, var1) 24 | else 25 | super 26 | end 27 | end 28 | 29 | def subtract(var1, var2) 30 | simplify_expression! factors = unite(convert(var1), convert(var2).reverse) 31 | simplify(*factors) || new(*factors) 32 | end 33 | 34 | def distributable?(var1, var2) 35 | simple?(var1) && var2.is_a?(Summands) 36 | end 37 | 38 | def distribute(var1, var2) 39 | var2.symbolic.map {|k,v| k*v }.inject(var2.numeric*var1) do |sum, it| 40 | sum + it*var1 41 | end 42 | end 43 | 44 | def simplify_expression!(factors) 45 | factors[1].delete_if {|base, exp| (base == IDENTITY) || (exp == 0) } 46 | factors[0] = 0 if factors[1].any? {|base, _| base == 0 } 47 | end 48 | 49 | def simplify(numeric, symbolic) 50 | if numeric == 0 || symbolic.empty? 51 | (numeric.round == numeric) ? numeric.to_i : numeric.to_f 52 | elsif numeric == IDENTITY && symbolic.size == 1 && symbolic.first[1] == 1 53 | symbolic.first[0] 54 | end 55 | end 56 | 57 | def unite_exponents(base, exponent) 58 | if base.is_a? Factors 59 | [base.numeric**exponent, Hash[*base.symbolic.map {|b,e| [b, e*exponent] }.flatten]] 60 | else 61 | [IDENTITY, { base => exponent }] 62 | end 63 | end 64 | end 65 | 66 | def reverse 67 | self.class.new numeric**-1, Hash[*symbolic.map {|k,v| [k,-v]}.flatten] 68 | end 69 | 70 | def value 71 | if variables.all?(&:value) 72 | @symbolic.inject(numeric) {|value, (base, exp)| value * base.value ** exp.value } 73 | end 74 | end 75 | 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 82 | end 83 | 84 | def value 85 | @symbolic.inject(@numeric){|m,(base,exponential)| m * base.value ** exponential.value} 86 | end 87 | 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 97 | end 98 | end 99 | end -------------------------------------------------------------------------------- /lib/symbolic/function.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | class Function 3 | attr_reader :name, :deriv, :operation 4 | 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 15 | 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 26 | 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 55 | end 56 | 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 106 | end 107 | end -------------------------------------------------------------------------------- /lib/symbolic/math.rb: -------------------------------------------------------------------------------- 1 | module Symbolic::Math 2 | =begin 3 | This module is a reflection for Math module which allows to use symbolic expressions as parameters for its methods. 4 | =end 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| 33 | instance_eval <<-CODE, __FILE__, __LINE__ + 1 34 | #{fctn}.set_operation(proc{|arg| ::Math.#{fctn.downcase}(arg)}) 35 | def #{fctn.downcase}(argument) 36 | #{fctn}[argument] 37 | end 38 | CODE 39 | end 40 | end -------------------------------------------------------------------------------- /lib/symbolic/misc.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | def factorial(n) 3 | (1..n).inject(1){|f,n| f*n} 4 | end 5 | end -------------------------------------------------------------------------------- /lib/symbolic/plugins/var_name.rb: -------------------------------------------------------------------------------- 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 43 | -------------------------------------------------------------------------------- /lib/symbolic/printer.rb: -------------------------------------------------------------------------------- 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 80 | -------------------------------------------------------------------------------- /lib/symbolic/statistics.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 2 | OPERATIONS = [:+, :-, :*, :/, :**, :-@] 3 | def operations 4 | formula = to_s 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 | } 17 | end 18 | end -------------------------------------------------------------------------------- /lib/symbolic/sum.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/symbolic/summands.rb: -------------------------------------------------------------------------------- 1 | # TODO: 2*symbolic is a 2 power of symbolic Summand 2 | require "#{File.dirname(__FILE__)}/expression.rb" 3 | module Symbolic 4 | require "#{File.dirname(__FILE__)}/expression.rb" 5 | class Summands < Expression 6 | OPERATION = :+ 7 | IDENTITY = 0 8 | class << self 9 | def summands(summands) 10 | summands 11 | end 12 | 13 | def factors(factors) 14 | if factors.symbolic.length == 1 && factors.symbolic.first[1] == Factors::IDENTITY 15 | new IDENTITY, factors.symbolic.first[0] => factors.numeric 16 | else 17 | new IDENTITY, Factors.new(1, factors.symbolic) => factors.numeric 18 | end 19 | end 20 | 21 | def simplify_expression!(summands) 22 | summands[1].delete_if {|base, coef| coef == 0 } 23 | end 24 | 25 | def simplify(numeric, symbolic) 26 | if symbolic.empty? #only the numeric portion 27 | numeric 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 44 | end 45 | end 46 | end 47 | 48 | def value 49 | if variables.all?(&:value) 50 | symbolic.inject(numeric) {|value, (base, coef)| value + base.value * coef.value } 51 | end 52 | end 53 | 54 | def reverse 55 | self.class.new( -numeric, Hash[*symbolic.map {|k,v| [k,-v]}.flatten] ) 56 | end 57 | def value 58 | @symbolic.inject(@numeric){|m,(base,coefficient)| m + coefficient * base.value} 59 | end 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 66 | end 67 | 68 | def diff(wrt) 69 | @symbolic.inject(0){|m,(base,coefficient)| m + coefficient * base.diff(wrt)} 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /lib/symbolic/variable.rb: -------------------------------------------------------------------------------- 1 | module Symbolic 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. 8 | =end 9 | class Variable 10 | include Symbolic 11 | attr_accessor :name, :proc 12 | attr_writer :value 13 | 14 | # Create a new Symbolic::Variable, with optional name, value and proc 15 | def initialize(options={}, &proc) 16 | (@name, @value), @proc = options.values_at(:name, :value), proc 17 | @name = @name.to_s if @name 18 | end 19 | 20 | def value 21 | @value || @proc && @proc.call.value 22 | end 23 | 24 | def variables 25 | [self] 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 43 | end 44 | end -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require 'simplecov' 3 | SimpleCov.start 4 | end 5 | 6 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 7 | require 'symbolic' 8 | require 'rspec' 9 | -------------------------------------------------------------------------------- /spec/symbolic/equal_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | describe Symbolic do 4 | let(:x) { var :name => :x, :value => 1 } 5 | 6 | it 'variables equality is instance equality' do 7 | x.should_not == var(:name => :x, :value => 1) 8 | end 9 | 10 | it 'should be equal (sub-groups)' do 11 | s = 3 + x 12 | a = s*3 13 | b = s*3 14 | a.should == a 15 | a.should == b 16 | b.should == a 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/symbolic_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', __FILE__) 2 | 3 | describe Symbolic do 4 | let(:var) { Object.new.extend Symbolic } 5 | 6 | it 'should handle unary plus' do 7 | (+var).should == var 8 | end 9 | 10 | it 'should handle unary minus' do 11 | Symbolic::Factors.should_receive(:add).with(var, -1).and_return(:negative_value) 12 | (-var).should == :negative_value 13 | end 14 | 15 | it 'should handle binary plus' do 16 | Symbolic::Summands.should_receive(:add).with(var, :other_var).and_return(:result) 17 | (var + :other_var).should == :result 18 | end 19 | 20 | it 'should handle binary minus' do 21 | Symbolic::Summands.should_receive(:subtract).with(var, :other_var).and_return(:result) 22 | (var - :other_var).should == :result 23 | end 24 | 25 | it 'should handle multiplication' do 26 | Symbolic::Factors.should_receive(:add).with(var, :other_var).and_return(:result) 27 | (var * :other_var).should == :result 28 | end 29 | 30 | it 'should handle division' do 31 | Symbolic::Factors.should_receive(:subtract).with(var, :other_var).and_return(:result) 32 | (var / :other_var).should == :result 33 | end 34 | 35 | it 'should handle exponentation' do 36 | Symbolic::Factors.should_receive(:power).with(var, :other_var).and_return(:result) 37 | (var ** :other_var).should == :result 38 | end 39 | 40 | it 'should delegate string representation to Printer' do 41 | Symbolic::Printer.should_receive(:print).with(var).and_return(:string) 42 | var.to_s.should == :string 43 | end 44 | 45 | it 'should redefine inspect' do 46 | var.should_receive(:to_s).and_return(:string) 47 | var.inspect.should == "Symbolic(string)" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /symbolic.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "symbolic" 8 | s.version = "0.3.7" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["brainopia"] 12 | s.date = "2012-02-26" 13 | s.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)." 14 | s.email = "ravwar@gmail.com" 15 | s.extra_rdoc_files = [ 16 | "README.rdoc" 17 | ] 18 | s.files = [ 19 | "README.rdoc", 20 | "Rakefile", 21 | "lib/symbolic.rb", 22 | "lib/symbolic/coerced.rb", 23 | "lib/symbolic/constant.rb", 24 | "lib/symbolic/constants.rb", 25 | "lib/symbolic/expression.rb", 26 | "lib/symbolic/extensions/kernel.rb", 27 | "lib/symbolic/extensions/matrix.rb", 28 | "lib/symbolic/extensions/module.rb", 29 | "lib/symbolic/extensions/numeric.rb", 30 | "lib/symbolic/extensions/rational.rb", 31 | "lib/symbolic/factors.rb", 32 | "lib/symbolic/function.rb", 33 | "lib/symbolic/math.rb", 34 | "lib/symbolic/misc.rb", 35 | "lib/symbolic/plugins/var_name.rb", 36 | "lib/symbolic/printer.rb", 37 | "lib/symbolic/statistics.rb", 38 | "lib/symbolic/sum.rb", 39 | "lib/symbolic/summands.rb", 40 | "lib/symbolic/variable.rb", 41 | "spec/integration_spec.rb", 42 | "spec/spec_helper.rb", 43 | "spec/symbolic/equal_spec.rb", 44 | "spec/symbolic_spec.rb", 45 | "symbolic.gemspec" 46 | ] 47 | s.homepage = "http://brainopia.github.com/symbolic" 48 | s.require_paths = ["lib"] 49 | s.rubygems_version = "1.8.11" 50 | s.summary = "Symbolic math for ruby" 51 | 52 | if s.respond_to? :specification_version then 53 | s.specification_version = 3 54 | 55 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 56 | s.add_development_dependency(%q, [">= 2.4"]) 57 | else 58 | s.add_dependency(%q, [">= 2.4"]) 59 | end 60 | else 61 | s.add_dependency(%q, [">= 2.4"]) 62 | end 63 | end 64 | 65 | --------------------------------------------------------------------------------