Skip to content

How to create an NMatrix

agarie edited this page Nov 4, 2014 · 2 revisions

OBSERVATION

This tutorial was written for an older version of NMatrix. While most of it still works fine, remember to read the documentation or consult the mailing list if any problem happens.

We should really think merge this document with the Tentative NMatrix Tutorial!


Let's start with the simplest thing possible: to create a NMatrix from an array of values, without any options:

>> m = N[ [2, 3, 4], [7, 8, 9] ]
=> #<NMatrix:0x007f8e121b6cf8shape:[2,3] dtype:int32 stype:dense> 
  [2, 3, 4]
  [7, 8, 9]
=> nil 

The type of a matrix's elements

Each NMatrix object has what's called a dtype. It's a Symbol that says what each element of the matrix is: if it's an integer, a floating-point, a complex or a rational number and the number of bits used to store it. These are defined in ext/nmatrix/nmatrix.h. Below is the complete list.

  • :byte (unsigned 8bit integer)
  • :int8 (signed 8bit integer, a char)
  • :int16
  • :int32
  • :int64
  • :float32
  • :float64
  • :complex64
  • :complex128
  • :rational32 # likely to be removed soon
  • :rational64 # likely to be removed soon
  • :rational128
  • :object (a ruby object, simply a VALUE)

They're very valuable when you know the kind of data you're going to use beforehand. If you're simply doing some experiments and have no idea what you're going to encounter, stick to the defaults - it's probably int64 or float64.

To create a matrix with a specific dtype, you can use #new or one of the various shortcuts that I've already talked about. Some examples:

>> NMatrix.new([2, 3], [0, 1, 2, 3, 4, 5], dtype: :int64)
  [0, 1, 2]
  [3, 4, 5]
=> nil 

Here the first parameter is an array representing the dimension of the matrix (2-by-3 in this case), the second parameter are the values used and the third one is the dtype. If you're creating a square matrix, you can simply pass an integer as the first parameter.

If there are less values than the number of element in the matrix, they will be repeated as much as necessary.

>> NMatrix.new([2, 3], [1, 7])
  [1, 7, 1]
  [7, 1, 7]
=> nil 

For complex and rational matrices, you must use Ruby's Complex and Rational classes.

>> values = []
>> 6.times { |i| values << Rational(i, 7) }
>> values
=> [(0/1), (1/7), (2/7), (3/7), (4/7), (5/7)] 

>> NMatrix.new([2, 3], values, :rational128)
  [(0/1), (1/7), (2/7)]
  [(3/7), (4/7), (5/7)]
=> nil

>> values = []
>> 6.times { |i| values << Complex(i, 2*i) }
>> values
=> [(0+0i), (1+2i), (2+4i), (3+6i), (4+8i), (5+10i)] 

>> NMatrix.new([2, 3], values, :complex64)
  [(0.0+0.0i), (1.0+2.0i), (2.0+4.0i)]
  [(3.0+6.0i), (4.0+8.0i), (5.0+10.0i)]
=> nil

If you don't specify a dtype, NMatrix will try to guess based on the first entry in the array you give it.

>> q = []
>> 4.times {|i| q << Rational(2, i+1)}
>> q
=> [(2/1), (1/1), (2/3), (1/2)] 

>> NMatrix.new(2, q).dtype
=> :rational64

And that's it for dtypes. As I said, most of the time we're dealing with integers and floating-point numbers, but it's very good to have this kind of machinery for the times when we need it - complex for lots of tasks in signal processing, rationals for number fields (e.g. [Q-lattices in quantum statistical mechanics][2]), uint8 for logical matrices (incidence matrices in graph theory), etc.

The storage of a matrix

The stype is a much less used parameter of NMatrix. It controls how the data of the matrix is stored on memory. It's defined as an enum in ext/nmatrix/nmatrix.h.

There are only 3 options:

  • :dense
  • :list
  • :yale

The first one is what you should use most of the time. It is the standard when creating a new matrix with #new or the shortcuts.

Yale is a standard format for sparse matrices. There are two versions of it: old Yale and new Yale, the difference being that the diagonal values are stored separately in the latter. There's a lot of information about it on Wikipedia.

NMatrix uses new Yale, which is defined in ext/nmatrix/storage/yale/. In general, it costs O(1) to look up a row in a Yale matrix, and O(log(n)) to look up an entry within a row (if there are n entries in the row). Inserting or removing values from the matrix causes a resize of the underlying storage, so modifying Yale matrices is expensive. They also lack the generalizability to exist in more (or less) than two dimensions.

Tall Yale matrices have less efficient storage but more efficient access; short Yale matrices have more efficient storage but less efficient access.

To avoid modifying Yale matrices, it's recommended that you use :list. List matrices store data in linked-lists, and may exist in any number of dimensions. They are defined in ext/nmatrix/storage/list.cpp.

There's also an experimental module called NMatrix::YaleFunctions with a lot of methods for reflection of Yale matrices: #yale_ija, #yale_ia, #yale_ja, #yale_d, #yale_lu, #yale_a, etc. These functions do not work on slices, but they're useful for debugging and understanding the space usage. You can add them to an NMatrix using n.extend NMatrix::YaleFunctions.

Conclusion

Understanding data and storage types makes it really simple to create new matrices:

NMatrix.new(shape, [initial_values, dtype: d, stype: s, capacity: c, default: d])

Only shape is required. capacity is useful if you want to pre-reserve space for a Yale matrix. The default keyword is there for sparse matrices, which prefer to not store zeros (but may alternatively store zeros instead of some other value, which is d).

Instead of providing an array for initial_values, you may provide a single value. If you go that route, that single value will be used as the default for Yale and List.

I hope this is all the information necessary for the most common use cases. If you have a suggestion, please post on SciRuby's mailing list.