MatteoRagni/cas-rb

View on GitHub
lib/operators/op.rb

Summary

Maintainability
A
2 hrs
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
  class CASError < RuntimeError; end

  #   ___
  #  / _ \ _ __
  # | (_) | '_ \
  #  \___/| .__/
  #       |_|
  class Op
    # Argument of the operation
    attr_reader :x

    # Initialize a new empty operation container. This is a virtual
    # class and the other must inherit from this basic container.
    # Some methods raise a `CAS::CASError` if called.
    # The input element is a Numric, to create a constant.
    # `CAS::Op` specifies operations with a single variable
    #
    #  * **argument**: `Numeric` to be converted in `CAS::Constant` or `CAS::Op` child operation
    #  * **returns**: `CAS::Op` instance
    def initialize(x)
      if x.is_a? Numeric
        x = Op.numeric_to_const x
      end
      CAS::Help.assert(x, CAS::Op)

      @x = x
    end

    def self.numeric_to_const(x)
      if CAS::NumericToConst[x]
        return CAS::NumericToConst[x]
      else
        return CAS::const x
      end
    end

    # Return the dependencies of the operation. Requires a `CAS::Variable`
    # and it is one of the recursve method (implicit tree resolution)
    #
    #  * **argument**: `CAS::Variable` instance
    #  * **returns**: `TrueClass` if depends, `FalseClass` if not
    def depend?(v)
      CAS::Help.assert(v, CAS::Op)

      @x.depend? v
    end

    # Return the derivative of the operation using the chain rule
    # The input is a `CAS::Op` because it can handle derivatives
    # with respect to functions. E.g.:
    #
    # ```
    #  f(x) = (ln(x))**2
    #  g(x) = ln(x)
    #
    #  d f(x)
    #  ------ = 2 ln(x)
    #  d g(x)
    # ```
    #
    #  * **argument**: `CAS::Op` object of the derivative
    #  * **returns**: `CAS::Op` a derivated object, or `CAS::Zero` for constants
    def diff(v)
      CAS::Help.assert(v, CAS::Op)

      if @x.depend? v
        return @x.diff(v)
      end
      CAS::Zero
    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
    #
    # ``` ruby
    # x, y = CAS::vars :x, :y
    # f = (x ** 2) + (y ** 2)
    # f.call({x => 1, y => 2})
    # # => 2
    # ```
    #
    #  * **argument**: `Hash` with feed dictionary
    #  * **returns**: `Numeric`
    def call(f)
      CAS::Help.assert(f, Hash)

      @x.call(f)
    end

    # Perform substitution of a part of the graph using a data table:
    #
    # ``` ruby
    # x, y = CAS::vars :x, :y
    # f = (x ** 2) + (y ** 2)
    # puts f
    # # => (x^2) + (y^2)
    # puts f.subs({x => CAS::ln(y)})
    # # => (ln(y)^2) + (y^2)
    # ```
    #
    #  * **argument**: `Hash` with substitution table
    #  * **returns**: `CAS::Op` (`self`) with substitution performed
    def subs(dt)
      CAS::Help.assert(dt, Hash)
      sub = dt.keys.select { |e| e == @x }[0]
      if sub
        if dt[sub].is_a? CAS::Op
          @x = dt[sub]
        elsif dt[sub].is_a? Numeric
          @x = CAS::const dt[sub]
        else
          raise CAS::CASError, "Impossible subs. Received a #{dt[@x].class} = #{dt[@x]}"
        end
      else
        @x.subs(dt)
      end
      return self
    end

    # Convert expression to string
    #
    #  * **returns**: `String` to print on screen
    def to_s
      "#{@x}"
    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}"
    end

    # Returns a sum of two `CAS::Op`s
    #
    #  * **argument**: `CAS::Op` tree
    #  * **returns**: `CAS::Op` new object
    def +(op)
      CAS::Sum.new [self, op]
    end

    # Returns a difference of two `CAS::Op`s
    #
    #  * **argument**: `CAS::Op` tree
    #  * **returns**: `CAS::Op` new object
    def -(op)
      CAS::Diff.new self, op
    end

    # Returns a product of two `CAS::Op`s
    #
    #  * **argument**: `CAS::Op` tree
    #  * **returns**: `CAS::Op` new object
    def *(op)
      CAS::Prod.new [self, op]
    end

    # Returns a division of two `CAS::Op`s
    #
    #  * **argument**: `CAS::Op` tree
    #  * **returns**: `CAS::Op` new object
    def /(op)
      CAS::Div.new self, op
    end

    # Returns the power of two `CAS::Op`s
    #
    #  * **argument**: `CAS::Op` tree
    #  * **returns**: `CAS::Op` new object
    def **(op)
      CAS.pow(self, op)
    end

    # Unary operator for inversion of a `CAS::Op`
    #
    #  * **returns**: `CAS::Op` new object
    def -@
      CAS.invert(self)
    end

    # Simplification callback. It simplify the subgraph of each node
    # until all possible simplification are performed (thus the execution
    # time is not deterministic).
    #
    #  * **returns**: `CAS::Op` simplified version
    def simplify
      hash = @x.to_s
      @x = @x.simplify
      while @x.to_s != hash
        hash = @x.to_s
        @x = @x.simplify
      end
    end

    # Simplify dictionary performs a dictionary simplification
    # that is the class variable `@simplify_dict`
    #
    #  * **returns**: `CAS::Op` self
    def simplify_dictionary
      self.class.simplify_dict(@x) || self
    end

    # Initializes the simplification dictionary (one for each class)
    #
    # * **returns**: `Hash` with simplification dictionary
    def self.init_simplify_dict
      @simplify_dict = { }
    end

    # Returns an element of a
    def self.simplify_dict(k)
      @simplify_dict.keys.each do |op|
        return @simplify_dict[op] if op.simplify == k.simplify
      end
      return nil
    end

    # Inspector for the current object
    #
    #  * **returns**: `String`
    def inspect
      "#{self.class}(#{@x.inspect})"
    end

    # Equality operator, the standard operator is overloaded
    # :warning: this operates on the graph, not on the math
    # See `CAS::equal`, etc.
    #
    #  * **argument**: `CAS::Op` to be tested against
    #  * **returns**: `TrueClass` if equal, `FalseClass` if differs
    def ==(op)
      # CAS::Help.assert(op, CAS::Op)
      if op.is_a? CAS::Op
        return false if op.is_a? CAS::BinaryOp
        return (self.class == op.class and @x == op.x)
      end
      false
    end

    # Disequality operator, the standard operator is overloaded
    # :warning: this operates on the graph, not on the math
    # See `CAS::equal`, etc.
    #
    #  * **argument**: `CAS::Op` to be tested against
    #  * **returns**: `FalseClass` if equal, `TrueClass` if differs
    def !=(op)
      not self.==(op)
    end

    # Evaluates the proc against a given context. It is like having a
    # snapshot of the tree transformed in a callable object.
    # Obviously **if the tree changes, the generated proc does notchanges**.
    # The proc takes as input a feed dictionary in which each variable
    # is identified through the `CAS::Variable#name` key.
    #
    # The proc is evaluated in the context devined by the input `Binding` object
    # If `nil` is passed, the `eval` will run in this local context
    #
    #  * **argument**: `Binding` or `NilClass` that is the context of the Ruby VM
    #  * **returns**: `Proc` object with a single argument as an `Hash`
    def as_proc(bind=nil)
      args_ext = self.args.map { |e| "#{e} = fd[\"#{e}\"];" }
      code = "Proc.new do |fd|; #{args_ext.join " "} #{self.to_code}; end"
      if bind # All objects have eval value, we bind when not nil
        # CAS::Help.assert(bind, Binding)
        bind.eval(code)
      else
        eval(code)
      end
    end

    # Returns a list of all `CAS::Variable`s of the current tree
    #
    #  * **returns**: `Array` of `CAS::Variable`s
    def args
      @x.args.uniq
    end
  end # Op
  CAS::Op.init_simplify_dict
end