Bug #1532
closedImproved matrix.rb [patch]
Description
=begin
The fact that the 'matrix' library is included in Ruby is very helpful.
There are currently some open bugs, and some improvements could make it easier to use.
Propositions:
- Matrix#trace should raise an ErrDimensionMismatch if the matrix is not square
Mathematically speaking, the trace is only defined for square matrices (ref: wikipedia, mathworld)
Currently, Matrix[[1,2]].trace raises a NoMethodError: undefined method `[]' for nil:NilClass
Note that Matrix[[1,2]].transpose.trace returns 1, although in mathematically, trace(M) = trace(transpose(M))
Raising an ErrDimensionMismatch would bring #trace in line with #inverse
- Matrix creation methods should perform checks and conversion so that values are stored as an Array of Arrays.
Currently, no checking is done, so the following will all be constructed without an error or a warning:
Matrix[:hello]
Matrix[nil]
Matrix[4], etc...
Later on, confusing results or strange errors will happen. E.g.:
Matrix[42].transpose # ==> Matrix[[42], [0], [0], [0]]
A TypeError should be raised if the argument is not convertible to an Array of Arrays.
Moreover, non arrays that should be converted using :to_ary, to be consistent
with the builtin library methods that accept array arguments (e.g. Array#transpose)
Finally, conversion from Vector to arrays should be always be performed. Currently,
a = Matrix[[1,2],[3,4]] # => Matrix[[1, 2], [3, 4]]
b = Matrix.rows([a.row(0), a.row(1)]) # => Matrix[Vector[1, 2], Vector[3, 4]]
a == b # => false
It would be more useful, intuitive and mathematically correct if a == b and b.to_s == "Matrix[[1, 2], [3, 4]]"
The same is true for Vector creation methods. For example currently:
Vector.elements(42, false).size # ==> 4
Vector.elements(Vector[1,2,3]) == Vector.elements([1,2,3]) # ==> false
It would be more useful, intuitive and correct if the first example raises an error and the second returns true
- Matrix creators should enforce that a matrix is rectangular.
Currently, a matrix with irregular rows can be created, e.g. x = Matrix[[1,2],[3]].
Mathematically speaking, that is not a matrix.
Basically none of the Matrix methods are of any use for non-rectangular matrices.
Moreover, many strange errors can occur later on. For example, x.inverse will
raise a "NoMethodError: undefined method `*' for nil:NilClass"
It would be helpful to catch these cases at creation time.
Many creation methods don't have to make any extra checks (e.g. Matrix.scalar), and
all methods of Matrix can bypass this extra check when they have created the arrays themselves
(e.g. Matrix#*). There would be a small cost for creation using Matrix.[] and Matrix.rows,
although it is in O(rows) while most other operations are usually in O(rows x columns),
so the performance difference would be minimal.
- Matrix should deal with empty matrices.
Currently, empty matrices like Matrix[] cause problem.
For example Matrix[].determinant raises an error, so does Matrix[] * Matrix[].
Moreover, if h = Matrix[[], []], then currently h.transpose.transpose != h
While an alternative would be to raise and error, the best solution is to handle
empty matrices properly, both 0x0, 0xn and nx0, as does MatLab, GNU Octave, etc...
See doc and references to the literature in:
http://www.gnu.org/software/octave/doc/interpreter/Empty-Matrices.html
- Out of bound indices should be dealt with.
a) Matrix#[row,col] should behave in a consisten way if either row or col is out of bounds.
Currently it returns nil vs raises an obscure error (See redmine #1518.)
b) Matrix[[1]].row(2) raises an obscure error, while Matrix[[1]].column(2) returns Vector[nil, nil]
c) In a similar vein, Matrix[[1]].minor(2..2,1..1) currently raises an error but not
Matrix[[1]].minor(1..1,2..2)
Solutions:
a) To be consistent with array lookup using [], Matrix#[] should return nil for out of bounds elements.
A #fetch method could be added, but the fact that matrices normally won't contain nil or false
makes it easy to deal with out of bounds references, e.g. m[r, c] || 0
b) Contrary to nil, it is not easy nor useful to deal with Vector[nil, nil, ...].
#row, and #column could raise an IndexError, but it is more useful and
more coherent with Array#at, etc... to return nil.
c) The same way Matrix#row and #col can be related to Array#at,
Matrix#minor should have similar semantics to Array#slice. If either starting point
is out of bounds, nil is returned. Otherwise a Matrix is returned, although it can
be smaller than what was requested. This is similar to
[:a, :b].slice(3..10) # => nil
[:a, :b].slice(2..10) # => []
[:a, :b].slice(1..10) # => [:b]
Matrix[[1], [2]].minor(0..10, 2..10) # => nil
Matrix[[1], [2]].minor(0..10, 1..10) # => Matrix[[], []]
Matrix[[1], [2]].minor(1..10, 0..10) # => Matrix[[2]]
- Matrix#collect, Vector#collect, #collect2, #map2 should return enumerators if no block is given
This would be more useful and is consistent with Array#each, etc...
- Matrix#hash should have less collisions
Currently, the following matrices have the same hash:
Matrix[]
Matrix[[0,0], [0,0]]
Matrix[[1,0], [0,1]]
Matrix[[42,42], [666,666]]
Matrix[[1,2,3,4], [5,6,1,2], [3,4,5,6]]
Ideally, these should have different hashes, since they are different matrices.
- Matrix#compare_by_row_vectors, Vector#compare_by and Vector#init_elements should be made private or disappear.
As per the documentation, these are not meant to be used.
As such it would be best if they didn't appear in the list of methods.
The attached patch addresses all these issues.
Moreover, it addresses all matrix-related issues I could find on redmine:
http://redmine.ruby-lang.org/issues/show/1020
http://redmine.ruby-lang.org/issues/show/1515
http://redmine.ruby-lang.org/issues/show/1516
http://redmine.ruby-lang.org/issues/show/1517
http://redmine.ruby-lang.org/issues/show/1518
http://redmine.ruby-lang.org/issues/show/1526
http://redmine.ruby-lang.org/issues/show/1531
Also fixed a bug with #determinant and #determinant_e that would raise an error for some matrices
(for instance any square matrix with m[0][0] == 0, e.g. Matrix[[0,1],[2,3]].determinant # => error raised)
Finally, the following methods are performing faster:
Matrix#collect
Matrix#transpose
Matrix#==
Matrix#eql?
Matrix#hash
Vector#collect
Vector#map2
Note that the branch 'runpaint' of rubyspecs has specs to this patch.
=end
Files