SciRuby/nmatrix

View on GitHub
lib/nmatrix/nmatrix.rb

Summary

Maintainability
F
4 days
Test Coverage
#--
# = NMatrix
#
# A linear algebra library for scientific computation in Ruby.
# NMatrix is part of SciRuby.
#
# NMatrix was originally inspired by and derived from NArray, by
# Masahiro Tanaka: http://narray.rubyforge.org
#
# == Copyright Information
#
# SciRuby is Copyright (c) 2010 - 2014, Ruby Science Foundation
# NMatrix is Copyright (c) 2012 - 2014, John Woods and the Ruby Science Foundation
#
# Please see LICENSE.txt for additional copyright notices.
#
# == Contributing
#
# By contributing source code to SciRuby, you agree to be bound by
# our Contributor Agreement:
#
# * https://github.com/SciRuby/sciruby/wiki/Contributor-Agreement
#
# == nmatrix.rb
#
# This file loads the C extension for NMatrix and all the ruby
# files and contains those core functionalities which can be
# implemented efficiently (or much more easily) in Ruby (e.g.,
# inspect, pretty_print, element-wise operations).
#++

# For some reason nmatrix.so ends up in a different place during gem build.

# Detect java
def jruby?
  /java/ === RUBY_PLATFORM
end

if jruby?
  require_relative 'jruby/nmatrix_java'
else
  if File.exist?("lib/nmatrix/nmatrix.so") #|| File.exist?("lib/nmatrix/nmatrix.bundle")
    # Development
    require_relative "nmatrix/nmatrix.so"
  else
    # Gem
    require_relative "../nmatrix.so"
    require_relative './io/mat_reader'
    require_relative './io/mat5_reader'
    require_relative './io/market'
    require_relative './io/point_cloud'

    require_relative './lapack_core.rb'
    require_relative './yale_functions.rb'
  end
end

require_relative './math.rb'
require_relative './monkeys'

# NMatrix is a matrix class that supports both multidimensional arrays
# (`:dense` stype) and sparse storage (`:list` or `:yale` stypes) and 13 data
# types, including complex numbers, various integer and
# floating-point sizes and ruby objects.
class NMatrix
  # Read and write extensions for NMatrix.
  module IO
    extend AutoloadPatch

    # Reader (and eventually writer) of Matlab .mat files.
    #
    # The .mat file format is documented in the following link:
    # * http://www.mathworks.com/help/pdf_doc/matlab/matfile_format.pdf
    module Matlab
      extend AutoloadPatch

      class << self
        # call-seq:
        #     load(mat_file_path) -> NMatrix
        #     load_mat(mat_file_path) -> NMatrix
        #
        # Load a .mat file and return a NMatrix corresponding to it.
        def load_mat(file_path)
          NMatrix::IO::Matlab::Mat5Reader.new(File.open(file_path, "rb+")).to_ruby
        end
        alias :load :load_mat
      end
    end
  end

  class << self
    # call-seq:
    #     load_matlab_file(path) -> Mat5Reader
    #
    # * *Arguments* :
    #   - +file_path+ -> The path to a version 5 .mat file.
    # * *Returns* :
    #   - A Mat5Reader object.
    def load_matlab_file(file_path)
      NMatrix::IO::Matlab::Mat5Reader.new(File.open(file_path, 'rb')).to_ruby
    end

    # call-seq:
    #     load_pcd_file(path) -> PointCloudReader::MetaReader
    #
    # * *Arguments* :
    #   - +file_path+ -> The path to a PCL PCD file.
    # * *Returns* :
    #   - A PointCloudReader::MetaReader object with the matrix stored in its +matrix+ property
    def load_pcd_file(file_path)
      NMatrix::IO::PointCloudReader::MetaReader.new(file_path)
    end

    # Calculate the size of an NMatrix of a given shape.
    def size(shape)
      shape = [shape,shape] unless shape.is_a?(Array)
      (0...shape.size).inject(1) { |x,i| x * shape[i] }
    end

    # Make N-D coordinate arrays for vectorized evaluations of
    # N-D scalar/vector fields over N-D grids, given N
    # coordinate arrays arrs. N > 1.
    #
    # call-seq:
    #     meshgrid(arrs) -> Array of NMatrix
    #     meshgrid(arrs, options) -> Array of NMatrix
    #
    # * *Arguments* :
    #   - +vectors+ -> Array of N coordinate arrays (Array or NMatrix), if any have more than one dimension they will be flatten
    #   - +options+ -> Hash with options (:sparse Boolean, false by default; :indexing Symbol, may be :ij or :xy, :xy by default)
    # * *Returns* :
    #   - Array of N N-D NMatrixes
    # * *Examples* :
    #     x, y = NMatrix::meshgrid([[1, [2, 3]], [4, 5]])
    #     x.to_a #<= [[1, 2, 3], [1, 2, 3]]
    #     y.to_a #<= [[4, 4, 4], [5, 5, 5]]
    #
    # * *Using* *options* :
    #
    #     x, y = NMatrix::meshgrid([[[1, 2], 3], [4, 5]], sparse: true)
    #     x.to_a #<= [[1, 2, 3]]
    #     y.to_a #<= [[4], [5]]
    #
    #     x, y = NMatrix::meshgrid([[1, 2, 3], [[4], 5]], indexing: :ij)
    #     x.to_a #<= [[1, 1], [2, 2], [3, 3]]
    #     y.to_a #<= [[4, 5], [4, 5], [4, 5]]
    def meshgrid(vectors, options = {})
      raise(ArgumentError, 'Expected at least 2 arrays.') if vectors.size < 2
      options[:indexing] ||= :xy
      raise(ArgumentError, 'Indexing must be :xy of :ij') unless [:ij, :xy].include? options[:indexing]
      mats = vectors.map { |arr| arr.respond_to?(:flatten) ? arr.flatten : arr.to_flat_array }
      mats[0], mats[1] = mats[1], mats[0] if options[:indexing] == :xy
      new_dim = mats.size
      lengths = mats.map(&:size)
      result = mats.map.with_index do |matrix, axis|
        if options[:sparse]
          new_shape = Array.new(new_dim, 1)
          new_shape[axis] = lengths[axis]
          new_elements = matrix
        else
          before_axis = lengths[0...axis].reduce(:*)
          after_axis = lengths[(axis+1)..-1].reduce(:*)
          new_shape = lengths
          new_elements = after_axis ? matrix.map{ |el| [el] * after_axis }.flatten : matrix
          new_elements *= before_axis if before_axis
        end
        NMatrix.new(new_shape, new_elements)
      end
      result[0], result[1] = result[1], result[0] if options[:indexing] == :xy
      result
    end
  end

  # TODO: Make this actually pretty.
  def pretty_print(q) #:nodoc:
    if self.shape.size > 1 and self.shape[1] > 100
      self.inspect.pretty_print(q)
    elsif self.dim > 3 || self.dim == 1
      self.to_a.pretty_print(q)
    else
      # iterate through the whole matrix and find the longest number
      longest = Array.new(self.shape[1], 0)
      self.each_column.with_index do |col, j|
        col.each do |elem|
          elem_len   = elem.inspect.size
          longest[j] = elem_len if longest[j] < elem_len
        end
      end

      if self.dim == 3
        q.group(0, "\n{ layers:", "}") do
          self.each_layer.with_index do |layer,k|
            q.group(0, "\n  [\n", "  ]\n") do
              layer.each_row.with_index do |row,i|
                q.group(0, "    [", "]\n") do
                  q.seplist(self[i,0...self.shape[1],k].to_flat_array, lambda { q.text ", "}, :each_with_index) { |v,j| q.text v.inspect.rjust(longest[j]) }
                end
              end
            end
          end
        end
      else # dim 2
        q.group(0, "\n[\n ", "]") do
          self.each_row.with_index do |row, i|
            q.group(1, " [", "]\n") do
              q.seplist(row.to_a, -> { q.text ", " }, :each_with_index) do |v,j|
                q.text v.inspect.rjust(longest[j])
              end
            end
            q.breakable unless i + 1 == self.shape[0]
          end
        end
      end
    end
  end

  #
  # call-seq:
  #     cast(stype, dtype, default) -> NMatrix
  #     cast(stype, dtype) -> NMatrix
  #     cast(stype) -> NMatrix
  #     cast(options) -> NMatrix
  #
  # This is a user-friendly helper for calling #cast_full. The easiest way to call this function is using an
  # options hash, e.g.,
  #
  #     n.cast(:stype => :yale, :dtype => :int64, :default => false)
  #
  # For list and yale, :default sets the "default value" or "init" of the matrix. List allows a bit more freedom
  # since non-zeros are permitted. For yale, unpredictable behavior may result if the value is not false, nil, or
  # some version of 0. Dense discards :default.
  #
  # dtype and stype are inferred from the matrix upon which #cast is called -- so you only really need to provide
  # one. You can actually call this function with no arguments, in which case it functions like #clone.
  #
  # If your dtype is :object and you are converting from :dense to a sparse type, it is recommended that you
  # provide a :default, as 0 may behave differently from its Float or Complex equivalent. If no option
  # is given, Integer 0 will be used.
  def cast(*params)
    if (params.size > 0 && params[0].is_a?(Hash))
      opts = {
          :stype => self.stype,
          :dtype => self.dtype,
          :default => self.stype == :dense ? 0 : self.default_value
      }.merge(params[0])

      self.cast_full(opts[:stype], opts[:dtype], opts[:default])
    else
      params << self.stype if params.size == 0
      params << self.dtype if params.size == 1
      #HACK: the default value can cause an exception if dtype is not complex
      #and default_value is. (The ruby C code apparently won't convert these.)
      #Perhaps this should be fixed in the C code (in rubyval_to_cval).
      default_value = maybe_get_noncomplex_default_value(params[1])
      params << (self.stype == :dense ? 0 : default_value) if params.size == 2
      self.cast_full(*params)
    end

  end


  #
  # call-seq:
  #     rows -> Integer
  #
  # This shortcut use #shape to return the number of rows (the first dimension)
  # of the matrix.
  #
  def rows
    shape[0]
  end

  #
  # call-seq:
  #     cols -> Integer
  #
  # This shortcut use #shape to return the number of columns (the second
  # dimension) of the matrix.
  #
  def cols
    shape[1]
  end

  # Return the main diagonal or antidiagonal a matrix. Only works with 2D matrices.
  #
  # == Arguments
  #
  # * +main_diagonal+ - Defaults to true. If passed 'false', then will return the
  #   antidiagonal of the matrix.
  #
  # == References
  #
  # * http://en.wikipedia.org/wiki/Main_diagonal
  def diagonal main_diagonal=true
    diag_size = [cols, rows].min
    diag = NMatrix.new [diag_size], dtype: dtype

    if main_diagonal
      0.upto(diag_size-1) do |i|
        diag[i] = self[i,i]
      end
    else
      row = 0
      (diag_size-1).downto(0) do |col|
        diag[row] = self[row,col]
        row += 1
      end
    end

    diag
  end

  #
  # call-seq:
  #     to_hash -> Hash
  #
  # Create a Ruby Hash from an NMatrix.
  #
  def to_hash
    if stype == :yale
      h = {}
      each_stored_with_indices do |val,i,j|
        next if val == 0 # Don't bother storing the diagonal zero values -- only non-zeros.
        if h.has_key?(i)
          h[i][j] = val
        else
          h[i] = {j => val}
        end
      end
      h
    else # dense and list should use a C internal function.
      # FIXME: Write a C internal to_h function.
      m = stype == :dense ? self.cast(:list, self.dtype) : self
      m.__list_to_hash__
    end
  end
  alias :to_h :to_hash


  def inspect #:nodoc:
    original_inspect = super()
    original_inspect = original_inspect[0...original_inspect.size-1]
    original_inspect + " " + inspect_helper.join(" ") + ">"
  end

  def __yale_ary__to_s(sym) #:nodoc:
    ary = self.send("__yale_#{sym.to_s}__".to_sym)

    '[' + ary.collect { |a| a ? a : 'nil'}.join(',') + ']'
  end


  # call-seq:
  #   integer_dtype?() -> Boolean
  #
  # Checks if dtype is an integer type
  #
  def integer_dtype?
    [:byte, :int8, :int16, :int32, :int64].include?(self.dtype)
  end

  # call-seq:
  #   float_dtype?() -> Boolean
  #
  # Checks if dtype is a floating point type
  #
  def float_dtype?
    [:float32, :float64].include?(dtype)
  end

  ##
  # call-seq:
  #   complex_dtype?() -> Boolean
  #
  # Checks if dtype is a complex type
  #
  def complex_dtype?
    [:complex64, :complex128].include?(self.dtype)
  end

  ##
  # call-seq:
  #
  # object_dtype?() -> Boolean
  #
  # Checks if dtype is a ruby object
  def object_dtype?
    dtype == :object
  end


  #
  # call-seq:
  #     to_f -> Float
  #
  # Converts an nmatrix with a single element (but any number of dimensions)
  #  to a float.
  #
  # Raises an IndexError if the matrix does not have just a single element.
  #
  def to_f
    raise IndexError, 'to_f only valid for matrices with a single element' unless shape.all? { |e| e == 1 }
    self[*Array.new(shape.size, 0)]
  end

  #
  # call-seq:
  #     to_flat_array -> Array
  #     to_flat_a -> Array
  #
  # Converts an NMatrix to a one-dimensional Ruby Array.
  #
  def to_flat_array
    ary = Array.new(self.size)
    self.each.with_index { |v,i| ary[i] = v }
    ary
  end
  alias :to_flat_a :to_flat_array

  #
  # call-seq:
  #     size -> Integer
  #
  # Returns the total size of the NMatrix based on its shape.
  #
  def size
    NMatrix.size(self.shape)
  end


  def to_s #:nodoc:
    self.to_flat_array.to_s
  end

  #
  # call-seq:
  #     nvector? -> true or false
  #
  # Shortcut function for determining whether the effective dimension is less than the dimension.
  # Useful when we take slices of n-dimensional matrices where n > 2.
  #
  def nvector?
    self.effective_dim < self.dim
  end

  #
  # call-seq:
  #     vector? -> true or false
  #
  # Shortcut function for determining whether the effective dimension is 1. See also #nvector?
  #
  def vector?
    self.effective_dim == 1
  end


  #
  # call-seq:
  #     to_a -> Array
  #
  # Converts an NMatrix to an array of arrays, or an NMatrix of effective dimension 1 to an array.
  #
  # Does not yet work for dimensions > 2
  def to_a(dimen=nil)
    if self.dim == 2

      return self.to_flat_a if self.shape[0] == 1

      ary = []
      begin
        self.each_row do |row|
          ary << row.to_flat_a
        end
      #rescue NotImplementedError # Oops. Try copying instead
      #  self.each_row(:copy) do |row|
      #    ary << row.to_a.flatten
      #  end
      end
      ary
    else
      to_a_rec(0)
    end
  end


  #
  # call-seq:
  #     rank(dimension, row_or_column_number) -> NMatrix
  #     rank(dimension, row_or_column_number, :reference) -> NMatrix reference slice
  #
  # Returns the rank (e.g., row, column, or layer) specified, using slicing by copy as default.
  #
  # See @row (dimension = 0), @column (dimension = 1)
  def rank(shape_idx, rank_idx, meth = :copy)

    if shape_idx > (self.dim-1)
      raise(RangeError, "#rank call was out of bounds")
    end

    params = Array.new(self.dim)
    params.each.with_index do |v,d|
      params[d] = d == shape_idx ? rank_idx : 0...self.shape[d]
    end

    meth == :reference ? self[*params] : self.slice(*params)
  end

  #
  # call-seq:
  #     column(column_number) -> NMatrix
  #     column(column_number, get_by) -> NMatrix
  #
  # Returns the column specified. Uses slicing by copy as default.
  #
  # * *Arguments* :
  #   - +column_number+ -> Integer.
  #   - +get_by+ -> Type of slicing to use, +:copy+ or +:reference+.
  # * *Returns* :
  #   - A NMatrix representing the requested column as a column vector.
  #
  # Examples:
  #
  #   m = NMatrix.new(2, [1, 4, 9, 14], :int32) # =>  1   4
  #                                                   9  14
  #
  #   m.column(1) # =>   4
  #                     14
  #
  def column(column_number, get_by = :copy)
    rank(1, column_number, get_by)
  end

  alias :col :column

  #
  # call-seq:
  #     row(row_number) -> NMatrix
  #     row(row_number, get_by) -> NMatrix
  #
  # * *Arguments* :
  #   - +row_number+ -> Integer.
  #   - +get_by+ -> Type of slicing to use, +:copy+ or +:reference+.
  # * *Returns* :
  #   - An NMatrix representing the requested row as a row vector.
  #
  def row(row_number, get_by = :copy)
    rank(0, row_number, get_by)
  end

  #
  # call-seq:
  #     last -> Element of self.dtype
  #
  # Returns the last element stored in an NMatrix
  #
  def last
    self[*Array.new(self.dim, -1)]
  end


  #
  # call-seq:
  #     reshape(new_shape) -> NMatrix
  #
  # Clone a matrix, changing the shape in the process. Note that this function does not do a resize; the product of
  # the new and old shapes' components must be equal.
  #
  # * *Arguments* :
  #   - +new_shape+ -> Array of positive Integers.
  # * *Returns* :
  #   - A copy with a different shape.
  #
  def reshape new_shape,*shapes
    if new_shape.is_a?Integer
      newer_shape =  [new_shape]+shapes
    else  # new_shape is an Array
      newer_shape = new_shape
    end
    t = reshape_clone_structure(newer_shape)
    left_params  = [:*]*newer_shape.size
    right_params = [:*]*self.shape.size
    t[*left_params] = self[*right_params]
    t
  end


  #
  # call-seq:
  #     reshape!(new_shape) -> NMatrix
  #     reshape! new_shape  -> NMatrix
  #
  # Reshapes the matrix (in-place) to the desired shape. Note that this function does not do a resize; the product of
  # the new and old shapes' components must be equal.
  #
  # * *Arguments* :
  #   - +new_shape+ -> Array of positive Integer.
  #
  def reshape! new_shape,*shapes
    if self.is_ref?
      raise(ArgumentError, "This operation cannot be performed on reference slices")
    else
      if new_shape.is_a?Integer
        shape =  [new_shape]+shapes
      else  # new_shape is an Array
        shape = new_shape
      end
      self.reshape_bang(shape)
    end
  end

  #
  # call-seq:
  #     transpose -> NMatrix
  #     transpose(permutation) -> NMatrix
  #
  # Clone a matrix, transposing it in the process. If the matrix is two-dimensional, the permutation is taken to be [1,0]
  # automatically (switch dimension 0 with dimension 1). If the matrix is n-dimensional, you must provide a permutation
  # of +0...n+.
  #
  # * *Arguments* :
  #   - +permutation+ -> Optional Array giving a permutation.
  # * *Returns* :
  #   - A copy of the matrix, but transposed.
  #
  def transpose(permute = nil)
    if permute.nil?
      if self.dim == 1
        return self.clone
      elsif self.dim == 2
        new_shape = [self.shape[1], self.shape[0]]
      else
        raise(ArgumentError, "need permutation array of size #{self.dim}")
      end
    elsif !permute.is_a?(Array) || permute.sort.uniq != (0...self.dim).to_a
      raise(ArgumentError, "invalid permutation array")
    else
      # Figure out the new shape based on the permutation given as an argument.
      new_shape = permute.map { |p| self.shape[p] }
    end

    if self.dim > 2 # FIXME: For dense, several of these are basically equivalent to reshape.

      # Make the new data structure.
      t = self.reshape_clone_structure(new_shape)

      self.each_stored_with_indices do |v,*indices|
        p_indices = permute.map { |p| indices[p] }
        t[*p_indices] = v
      end
      t
    elsif self.list? # TODO: Need a C list transposition algorithm.
      # Make the new data structure.
      t = self.reshape_clone_structure(new_shape)

      self.each_column.with_index do |col,j|
        t[j,:*] = col.to_flat_array
      end
      t
    else
      # Call C versions of Yale and List transpose, which do their own copies
      if jruby?
        nmatrix = NMatrix.new :copy
        nmatrix.shape = [@shape[1],@shape[0]]
        twoDMat = self.twoDMat.transpose
        nmatrix.s = ArrayRealVector.new(ArrayGenerator.getArrayDouble(twoDMat.getData(), shape[1],shape[0]))
        return nmatrix
      else
        self.clone_transpose
      end
    end
  end


  # call-seq:
  #     matrix1.concat(*m2) -> NMatrix
  #     matrix1.concat(*m2, rank) -> NMatrix
  #     matrix1.hconcat(*m2) -> NMatrix
  #     matrix1.vconcat(*m2) -> NMatrix
  #     matrix1.dconcat(*m3) -> NMatrix
  #
  # Joins two matrices together into a new larger matrix. Attempts to determine
  # which direction to concatenate on by looking for the first common element
  # of the matrix +shape+ in reverse. In other words, concatenating two columns
  # together without supplying +rank+ will glue them into an n x 2 matrix.
  #
  # You can also use hconcat, vconcat, and dconcat for the first three ranks.
  # concat performs an hconcat when no rank argument is provided.
  #
  # The two matrices must have the same +dim+.
  #
  # * *Arguments* :
  #   - +matrices+ -> one or more matrices
  #   - +rank+ -> Integer (for rank); alternatively, may use :row, :column, or
  #   :layer for 0, 1, 2, respectively
  def concat(*matrices)
    rank = nil
    rank = matrices.pop unless matrices.last.is_a?(NMatrix)

    # Find the first matching dimension and concatenate along that (unless rank is specified)
    if rank.nil?
      rank = self.dim-1
      self.shape.reverse_each.with_index do |s,i|
        matrices.each do |m|
          if m.shape[i] != s
            rank -= 1
            break
          end
        end
      end
    elsif rank.is_a?(Symbol) # Convert to numeric
      rank = {:row => 0, :column => 1, :col => 1, :lay => 2, :layer => 2}[rank]
    end

    # Need to figure out the new shape.
    new_shape = self.shape.dup
    new_shape[rank] = matrices.inject(self.shape[rank]) { |total,m| total + m.shape[rank] }

    # Now figure out the options for constructing the concatenated matrix.
    opts = {stype: self.stype, default: self.default_value, dtype: self.dtype}
    if self.yale?
      # We can generally predict the new capacity for Yale. Subtract out the number of rows
      # for each matrix being concatenated, and then add in the number of rows for the new
      # shape. That takes care of the diagonal. The rest of the capacity is represented by
      # the non-diagonal non-default values.
      new_cap = matrices.inject(self.capacity - self.shape[0]) do |total,m|
        total + m.capacity - m.shape[0]
      end - self.shape[0] + new_shape[0]
      opts = {capacity: new_cap}.merge(opts)
    end

    # Do the actual construction.
    n = NMatrix.new(new_shape, opts)

    # Figure out where to start concatenation. We don't know where it will end,
    # because each matrix may have own size along concat dimension.
    pos = Array.new(self.dim) { 0 }

    matrices.unshift(self)
    matrices.each do |m|
      # Figure out where to start and stop the concatenation. We'll use
      # NMatrices instead of Arrays because then we can do elementwise addition.
      ranges = m.shape.map.with_index { |s,i| pos[i]...(pos[i] + s) }

      n[*ranges] = m

      # Move over by the requisite amount
      pos[rank] = pos[rank] + m.shape[rank]
    end

    n
  end

  # Horizontal concatenation with +matrices+.
  def hconcat(*matrices)
    concat(*matrices, :column)
  end

  # Vertical concatenation with +matrices+.
  def vconcat(*matrices)
    concat(*matrices, :row)
  end

  # Depth concatenation with +matrices+.
  def dconcat(*matrices)
    concat(*matrices, :layer)
  end


  #
  # call-seq:
  #     upper_triangle -> NMatrix
  #     upper_triangle(k) -> NMatrix
  #     triu -> NMatrix
  #     triu(k) -> NMatrix
  #
  # Returns the upper triangular portion of a matrix. This is analogous to the +triu+ method
  # in MATLAB.
  #
  # * *Arguments* :
  #   - +k+ -> Positive integer. How many extra diagonals to include in the upper triangular portion.
  #
  def upper_triangle(k = 0)
    raise(NotImplementedError, "only implemented for 2D matrices") if self.shape.size > 2

    t = self.clone_structure
    (0...self.shape[0]).each do |i|
      if i - k < 0
        t[i, :*] = self[i, :*]
      else
        t[i, 0...(i-k)]             = 0
        t[i, (i-k)...self.shape[1]] = self[i, (i-k)...self.shape[1]]
      end
    end
    t
  end
  alias :triu :upper_triangle


  #
  # call-seq:
  #     upper_triangle! -> NMatrix
  #     upper_triangle!(k) -> NMatrix
  #     triu! -> NMatrix
  #     triu!(k) -> NMatrix
  #
  # Deletes the lower triangular portion of the matrix (in-place) so only the upper portion remains.
  #
  # * *Arguments* :
  #   - +k+ -> Integer. How many extra diagonals to include in the deletion.
  #
  def upper_triangle!(k = 0)
    raise(NotImplementedError, "only implemented for 2D matrices") if self.shape.size > 2

    (0...self.shape[0]).each do |i|
      if i - k >= 0
        self[i, 0...(i-k)] = 0
      end
    end
    self
  end
  alias :triu! :upper_triangle!


  #
  # call-seq:
  #     lower_triangle -> NMatrix
  #     lower_triangle(k) -> NMatrix
  #     tril -> NMatrix
  #     tril(k) -> NMatrix
  #
  # Returns the lower triangular portion of a matrix. This is analogous to the +tril+ method
  # in MATLAB.
  #
  # * *Arguments* :
  #   - +k+ -> Integer. How many extra diagonals to include in the lower triangular portion.
  #
  def lower_triangle(k = 0)
    raise(NotImplementedError, "only implemented for 2D matrices") if self.shape.size > 2

    t = self.clone_structure
    (0...self.shape[0]).each do |i|
      if i + k >= shape[0]
        t[i, :*] = self[i, :*]
      else
        t[i, (i+k+1)...self.shape[1]] = 0
        t[i, 0..(i+k)] = self[i, 0..(i+k)]
      end
    end
    t
  end
  alias :tril :lower_triangle


  #
  # call-seq:
  #     lower_triangle! -> NMatrix
  #     lower_triangle!(k) -> NMatrix
  #     tril! -> NMatrix
  #     tril!(k) -> NMatrix
  #
  # Deletes the upper triangular portion of the matrix (in-place) so only the lower portion remains.
  #
  # * *Arguments* :
  #   - +k+ -> Integer. How many extra diagonals to include in the deletion.
  #
  def lower_triangle!(k = 0)
    raise(NotImplementedError, "only implemented for 2D matrices") if self.shape.size > 2

    (0...self.shape[0]).each do |i|
      if i + k < shape[0]
        self[i, (i+k+1)...self.shape[1]] = 0
      end
    end
    self
  end
  alias :tril! :lower_triangle!


  #
  # call-seq:
  #     layer(layer_number) -> NMatrix
  #     row(layer_number, get_by) -> NMatrix
  #
  # * *Arguments* :
  #   - +layer_number+ -> Integer.
  #   - +get_by+ -> Type of slicing to use, +:copy+ or +:reference+.
  # * *Returns* :
  #   - A NMatrix representing the requested layer as a layer vector.
  #
  def layer(layer_number, get_by = :copy)
    layer = rank(2, layer_number, get_by)

    if jruby?
      nmatrix = NMatrix.new :copy
      nmatrix.shape = layer.shape
      nmatrix.s = layer.s
      return nmatrix
    else
      layer
    end

  end



  #
  # call-seq:
  #     shuffle! -> ...
  #     shuffle!(random: rng) -> ...
  #
  # Re-arranges the contents of an NVector.
  #
  # TODO: Write more efficient version for Yale, list.
  # TODO: Generalize for more dimensions.
  def shuffle!(*args)
    method_missing(:shuffle!, *args) if self.effective_dim > 1
    ary = self.to_flat_a
    ary.shuffle!(*args)
    ary.each.with_index { |v,idx| self[idx] = v }
    self
  end


  #
  # call-seq:
  #     shuffle -> ...
  #     shuffle(rng) -> ...
  #
  # Re-arranges the contents of an NVector.
  #
  # TODO: Write more efficient version for Yale, list.
  # TODO: Generalize for more dimensions.
  def shuffle(*args)
    method_missing(:shuffle!, *args) if self.effective_dim > 1
    t = self.clone
    t.shuffle!(*args)
  end


  #
  # call-seq:
  #     sorted_indices -> Array
  #
  # Returns an array of the indices ordered by value sorted.
  #
  def sorted_indices
    return method_missing(:sorted_indices) unless vector?
    ary = self.to_flat_array
    ary.each_index.sort_by { |i| ary[i] }  # from: http://stackoverflow.com/a/17841159/170300
  end


  #
  # call-seq:
  #     binned_sorted_indices -> Array
  #
  # Returns an array of arrays of indices ordered by value sorted. Functions basically like +sorted_indices+, but
  # groups indices together for those values that are the same.
  #
  def binned_sorted_indices
    return method_missing(:sorted_indices) unless vector?
    ary = self.to_flat_array
    ary2 = []
    last_bin = ary.each_index.sort_by { |i| [ary[i]] }.inject([]) do |result, element|
      if result.empty? || ary[result[-1]] == ary[element]
        result << element
      else
        ary2 << result
        [element]
      end
    end
    ary2 << last_bin unless last_bin.empty?
    ary2
  end


  def method_missing name, *args, &block #:nodoc:
    if name.to_s =~ /^__list_elementwise_.*__$/
      raise NotImplementedError, "requested undefined list matrix element-wise operation"
    elsif name.to_s =~ /^__yale_scalar_.*__$/
      raise NotImplementedError, "requested undefined yale scalar element-wise operation"
    else
      super(name, *args, &block)
    end
  end


  def respond_to?(method, include_all = false) #:nodoc:
    if [:shuffle, :shuffle!, :each_with_index, :sorted_indices, :binned_sorted_indices, :nrm2, :asum].include?(method.intern) # vector-only methods
      return vector?
    elsif [:each_layer, :layer].include?(method.intern) # 3-or-more dimensions only
      return dim > 2
    else
      super
    end
  end


  #
  # call-seq:
  #     inject -> symbol
  #
  # This overrides the inject function to use map_stored for yale matrices
  #
  def inject(sym)
    return super(sym) unless self.yale?
    return self.map_stored.inject(sym)
  end

  # Returns the index of the first occurence of the specified value. Returns
  # an array containing the position of the value, nil in case the value is not found.
  #
  def index(value)
    index = nil

    self.each_with_indices do |yields|
      if yields.first == value
        yields.shift
        index = yields
        break
      end
    end

    index
  end

  #
  # call-seq:
  #     clone_structure -> NMatrix
  #
  # This function is like clone, but it only copies the structure and the default value.
  # None of the other values are copied. It takes an optional capacity argument. This is
  # mostly only useful for dense, where you may not want to initialize; for other types,
  # you should probably use +zeros_like+.
  #
  def clone_structure(capacity = nil)
    opts = {stype: self.stype, default: self.default_value, dtype: self.dtype}
    opts = {capacity: capacity}.merge(opts) if self.yale?
    NMatrix.new(self.shape, opts)
  end

  #
  # call-seq:
  #     repeat(count, axis) -> NMatrix
  #
  # * *Arguments* :
  #   - +count+ -> how many times NMatrix should be repeated
  #   - +axis+ -> index of axis along which NMatrix should be repeated
  # * *Returns* :
  #   - NMatrix created by repeating the existing one along an axis
  # * *Examples* :
  #     m = NMatrix.new([2, 2], [1, 2, 3, 4])
  #     m.repeat(2, 0).to_a #<= [[1, 2], [3, 4], [1, 2], [3, 4]]
  #     m.repeat(2, 1).to_a #<= [[1, 2, 1, 2], [3, 4, 3, 4]]
  def repeat(count, axis)
    raise(ArgumentError, 'Matrix should be repeated at least 2 times.') if count < 2
    new_shape = shape
    new_shape[axis] *= count
    new_matrix = NMatrix.new(new_shape, dtype: dtype)
    slice = new_shape.map { |axis_size| 0...axis_size }
    start = 0
    count.times do
      slice[axis] = start...(start += shape[axis])
      new_matrix[*slice] = self
    end
    new_matrix
  end

  # This is how you write an individual element-wise operation function:
  #def __list_elementwise_add__ rhs
  #  self.__list_map_merged_stored__(rhs){ |l,r| l+r }.cast(self.stype, NMatrix.upcast(self.dtype, rhs.dtype))
  #end
protected

  def inspect_helper #:nodoc:
    ary = []
    ary << "shape:[#{shape.join(',')}]" << "dtype:#{dtype}" << "stype:#{stype}"

    if stype == :yale
      ary << "capacity:#{capacity}"

      # These are enabled by the DEBUG_YALE compiler flag in extconf.rb.
      if respond_to?(:__yale_a__)
        ary << "ija:#{__yale_ary__to_s(:ija)}" << "ia:#{__yale_ary__to_s(:ia)}" <<
          "ja:#{__yale_ary__to_s(:ja)}" << "a:#{__yale_ary__to_s(:a)}" << "d:#{__yale_ary__to_s(:d)}" <<
          "lu:#{__yale_ary__to_s(:lu)}" << "yale_size:#{__yale_size__}"
      end

    end

    ary
  end


  # Clone the structure as needed for a reshape
  def reshape_clone_structure(new_shape) #:nodoc:
    raise(ArgumentError, "reshape cannot resize; size of new and old matrices must match") unless self.size == new_shape.inject(1) { |p,i| p *= i }

    opts = {stype: self.stype, default: self.default_value, dtype: self.dtype}
    if self.yale?
      # We can generally predict the change in capacity for Yale.
      opts = {capacity: self.capacity - self.shape[0] + new_shape[0]}.merge(opts)
    end
    NMatrix.new(new_shape, opts)
  end


  # Helper for converting a matrix into an array of arrays recursively
  def to_a_rec(dimen = 0) #:nodoc:
    return self.flat_map { |v| v } if dimen == self.dim-1

    ary = []
    self.each_rank(dimen) do |sect|
      ary << sect.to_a_rec(dimen+1)
    end
    ary
  end


  # NMatrix constructor helper for sparse matrices. Uses multi-slice-setting to initialize a matrix
  # with a given array of initial values.
  def __sparse_initial_set__(ary) #:nodoc:
    self[0...self.shape[0],0...self.shape[1]] = ary
  end


  # This function assumes that the shapes of the two matrices have already
  # been tested and are the same.
  #
  # Called from inside NMatrix: nm_eqeq
  #
  # There are probably more efficient ways to do this, but currently it's unclear how.
  # We could use +each_row+, but for list matrices, it's still going to need to make a
  # reference to each of those rows, and that is going to require a seek.
  #
  # It might be more efficient to convert one sparse matrix type to the other with a
  # cast and then run the comparison. For now, let's assume that people aren't going
  # to be doing this very often, and we can optimize as needed.
  def dense_eql_sparse? m #:nodoc:
    m.each_with_indices do |v,*indices|
      return false if self[*indices] != v
    end

    return true
  end
  alias :sparse_eql_sparse? :dense_eql_sparse?


  #
  # See the note in #cast about why this is necessary.
  # If this is a non-dense matrix with a complex dtype and to_dtype is
  # non-complex, then this will convert the default value to noncomplex.
  # Returns 0 if dense.  Returns existing default_value if there isn't a
  # mismatch.
  #
  def maybe_get_noncomplex_default_value(to_dtype) #:nodoc:
    default_value = 0
    unless self.stype == :dense then
      if self.dtype.to_s.start_with?('complex') and not to_dtype.to_s.start_with?('complex') then
        default_value = self.default_value.real
      else
        default_value = self.default_value
      end
    end
    default_value
  end

end

require_relative './shortcuts.rb'
require_relative './enumerate.rb'

require_relative './version.rb'
require_relative './blas.rb'