MatteoRagni/cas-rb

View on GitHub
lib/functions/fnc-sum.rb

Summary

Maintainability
A
1 hr
Test Coverage
#!/usr/bin/env ruby

# Copyright (c) 2016 Matteo Ragni
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

module CAS
  #  ___
  # / __|_  _ _ __
  # \__ \ || | '  \
  # |___/\_,_|_|_|_|

  ##
  # **Sum basic operation**. As for now it is implemented as a simple
  # binary operation. It will be implemented as n-ary op.
  class Sum < CAS::NaryOp
    # Performs the sum between arbitrary number of `CAS::Op`
    #
    # ```
    #   d
    # ---- (f(x) + g(x) + h(x)) = f'(x) + g'(x) + h'(x)
    #  dx
    # ```
    #
    #  * **argument**: `CAS::Op` argument of derivative
    #  * **returns**: `CAS::Op` derivative
    def diff(v)
      @x.map { |x| x.diff(v) }.inject { |sum_x, dx| sum_x += dx }
    end

    # The added element of a sum accumulates inside the
    # vector that holds the elements
    def +(op)
      CAS::Help.assert(op, CAS::Op)
      @x << op
      self
    end

    # Call resolves the operation tree in a `Numeric` (if `Fixnum`)
    # or `Float` (depends upon promotions).
    # As input, it requires an hash with `CAS::Variable` or `CAS::Variable#name`
    # as keys, and a `Numeric` as a value. In this case it will call
    # the `Fixnum#overloaded_plus`, that is the old plus function.
    #
    #  * **argument**: `Hash` with feed dictionary
    #  * **returns**: `Numeric`
    def call(f)
      CAS::Help.assert(f, Hash)
      p = 0
      @x.each do |y|
        p = p.overloaded_plus(y.call(f))
      end
      p
    end

    # Convert expression to string
    #
    #  * **returns**: `String` to print on screen
    def to_s
      "(#{@x.map(&:to_s).join(" + ")})"
    end

    # Same as `CAS::Op`
    #
    # Simplifcation engine supports:
    #
    #  * x + 0 = x
    #  * 0 + y = y
    #  * x + x = 2 x
    #  * x + (-x) = 0
    #  * x + (-y) = x - y
    #  * 1 + 2 = 3 (constants reduction)
    #
    #  * **returns**: `CAS::Op` simplified version
    def simplify
      super
      return @x[0] if @x.size == 1

      # return CAS::Zero if @x == -@y or -@x == @y
      # return (@x - @y.x) if @y.is_a? CAS::Invert
      # return CAS.const(self.call({})) if (@x.is_a? CAS::Constant and @y.is_a? CAS::Constant)
      # Removing Zeros
      @x = @x - [CAS::Zero]
      return CAS::Zero if @x.size == 0
      # Reduce constants
      @x = self.__reduce_constants(@x) do |cs, xs|
        xs + [cs.inject { |t, c| t += c.call({}) }]
      end
      # Multeplicity and associativity executed
      return self.reduce_associativity
    end

    # Reduces from an associative point of view, by a segregation
    # of "negative" and positive elements. Negatives comes from
    # Diff operations and Invert operations. All the others are considered
    # positive. This function implements an internal heuristic. Should
    # not be used outside
    #
    #  * **returns**: A `CAS::Diff` or a `CAS::Sum`
    def reduce_associativity
      pos, neg = [], []

      @x.each do |x_el|
        case x_el
        when CAS::Invert
          neg << x_el.x
        when CAS::Diff
          pos << x_el.x
          neg << x_el.y
        else
          pos << x_el
        end
      end

      pos, neg = self.reduce_associativity_array pos, neg
      pos = self.__reduce_multeplicity(pos)
      neg = self.__reduce_multeplicity neg

      # TODO : Add rules for simplifications
      left, right = nil, nil
      left  = CAS::Sum.new(pos) if pos.size > 1
      left  = pos[0]            if pos.size == 1
      right = CAS::Sum.new(neg) if neg.size > 1
      right = neg[0]            if neg.size == 1

      return  CAS::Zero unless left || right
      return  left unless right
      return  -right unless left
      return left - right
    end

    # Reduce the positive and the negative associative part of
    # the sum to perform the symbolic difference. Does not take into account
    # multeplicity
    #
    #  * **requires**: positive `Array`
    #  * **requires**: negative `Array`
    #  * **returns**: positive, reduced `Array` and negative `Array`
    def reduce_associativity_array(p_old, n_old)
      p_del, n_del = [], []
      p_old.each do |p|
        n_old.each do |n|
          if p == n
            p_del << p
            n_del << n
          end
        end
      end

      return (p_old - p_del), (n_old - n_del)
    end

    # Convert expression to code (internal, for `CAS::Op#to_proc` method)
    #
    #  * **returns**: `String` that represent Ruby code to be parsed in `CAS::Op#to_proc`
    def to_code
      "(#{@x.map(&:to_code).join(" + ")})"
    end
  end # Sum
  CAS::Sum.init_simplify_dict
end