nmatrix 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/ext/nmatrix/data/complex.h +20 -55
  3. data/ext/nmatrix/data/data.cpp +11 -44
  4. data/ext/nmatrix/data/data.h +174 -311
  5. data/ext/nmatrix/data/meta.h +1 -7
  6. data/ext/nmatrix/data/ruby_object.h +3 -85
  7. data/ext/nmatrix/extconf.rb +2 -73
  8. data/ext/nmatrix/math.cpp +170 -813
  9. data/ext/nmatrix/math/asum.h +2 -25
  10. data/ext/nmatrix/math/{inc.h → cblas_enums.h} +11 -22
  11. data/ext/nmatrix/math/cblas_templates_core.h +507 -0
  12. data/ext/nmatrix/math/gemm.h +2 -32
  13. data/ext/nmatrix/math/gemv.h +1 -35
  14. data/ext/nmatrix/math/getrf.h +21 -6
  15. data/ext/nmatrix/math/getrs.h +0 -8
  16. data/ext/nmatrix/math/imax.h +0 -22
  17. data/ext/nmatrix/math/long_dtype.h +0 -3
  18. data/ext/nmatrix/math/math.h +11 -337
  19. data/ext/nmatrix/math/nrm2.h +2 -23
  20. data/ext/nmatrix/math/rot.h +1 -25
  21. data/ext/nmatrix/math/rotg.h +4 -13
  22. data/ext/nmatrix/math/scal.h +0 -22
  23. data/ext/nmatrix/math/trsm.h +0 -55
  24. data/ext/nmatrix/math/util.h +148 -0
  25. data/ext/nmatrix/nmatrix.cpp +0 -14
  26. data/ext/nmatrix/nmatrix.h +92 -84
  27. data/ext/nmatrix/ruby_constants.cpp +0 -2
  28. data/ext/nmatrix/ruby_constants.h +0 -2
  29. data/ext/nmatrix/ruby_nmatrix.c +86 -45
  30. data/ext/nmatrix/storage/dense/dense.cpp +1 -7
  31. data/ext/nmatrix/storage/storage.h +0 -1
  32. data/ext/nmatrix/ttable_helper.rb +0 -6
  33. data/ext/nmatrix/util/io.cpp +1 -1
  34. data/lib/nmatrix.rb +1 -19
  35. data/lib/nmatrix/blas.rb +33 -11
  36. data/lib/nmatrix/io/market.rb +3 -3
  37. data/lib/nmatrix/lapack_core.rb +181 -0
  38. data/lib/nmatrix/lapack_plugin.rb +44 -0
  39. data/lib/nmatrix/math.rb +382 -131
  40. data/lib/nmatrix/monkeys.rb +2 -3
  41. data/lib/nmatrix/nmatrix.rb +166 -13
  42. data/lib/nmatrix/shortcuts.rb +72 -7
  43. data/lib/nmatrix/version.rb +2 -2
  44. data/spec/00_nmatrix_spec.rb +154 -5
  45. data/spec/02_slice_spec.rb +2 -6
  46. data/spec/03_nmatrix_monkeys_spec.rb +7 -1
  47. data/spec/blas_spec.rb +60 -33
  48. data/spec/homogeneous_spec.rb +10 -10
  49. data/spec/lapack_core_spec.rb +482 -0
  50. data/spec/math_spec.rb +436 -52
  51. data/spec/shortcuts_spec.rb +28 -4
  52. data/spec/spec_helper.rb +14 -2
  53. data/spec/utm5940.mtx +83844 -0
  54. metadata +49 -76
  55. data/.gitignore +0 -27
  56. data/.rspec +0 -2
  57. data/.travis.yml +0 -15
  58. data/CONTRIBUTING.md +0 -82
  59. data/Gemfile +0 -2
  60. data/History.txt +0 -677
  61. data/LICENSE.txt +0 -23
  62. data/Manifest.txt +0 -92
  63. data/README.rdoc +0 -150
  64. data/Rakefile +0 -216
  65. data/ext/nmatrix/data/rational.h +0 -440
  66. data/ext/nmatrix/math/geev.h +0 -82
  67. data/ext/nmatrix/math/ger.h +0 -96
  68. data/ext/nmatrix/math/gesdd.h +0 -80
  69. data/ext/nmatrix/math/gesvd.h +0 -78
  70. data/ext/nmatrix/math/getf2.h +0 -86
  71. data/ext/nmatrix/math/getri.h +0 -108
  72. data/ext/nmatrix/math/potrs.h +0 -129
  73. data/ext/nmatrix/math/swap.h +0 -52
  74. data/lib/nmatrix/lapack.rb +0 -240
  75. data/nmatrix.gemspec +0 -55
  76. data/scripts/mac-brew-gcc.sh +0 -50
  77. data/scripts/mac-mavericks-brew-gcc.sh +0 -22
  78. data/spec/lapack_spec.rb +0 -459
@@ -0,0 +1,181 @@
1
+ #--
2
+ # = NMatrix
3
+ #
4
+ # A linear algebra library for scientific computation in Ruby.
5
+ # NMatrix is part of SciRuby.
6
+ #
7
+ # NMatrix was originally inspired by and derived from NArray, by
8
+ # Masahiro Tanaka: http://narray.rubyforge.org
9
+ #
10
+ # == Copyright Information
11
+ #
12
+ # SciRuby is Copyright (c) 2010 - 2014, Ruby Science Foundation
13
+ # NMatrix is Copyright (c) 2012 - 2014, John Woods and the Ruby Science Foundation
14
+ #
15
+ # Please see LICENSE.txt for additional copyright notices.
16
+ #
17
+ # == Contributing
18
+ #
19
+ # By contributing source code to SciRuby, you agree to be bound by
20
+ # our Contributor Agreement:
21
+ #
22
+ # * https://github.com/SciRuby/sciruby/wiki/Contributor-Agreement
23
+ #
24
+ # == lapack_core.rb
25
+ #
26
+ # This file contains friendlier interfaces to LAPACK functions
27
+ # implemented in C.
28
+ # This file is only for functions available with the core nmatrix gem
29
+ # (no external libraries needed).
30
+ #
31
+ # Note: most of these functions are borrowed from ATLAS, which is available under a BSD-
32
+ # style license.
33
+ #++
34
+
35
+ class NMatrix
36
+
37
+ module LAPACK
38
+
39
+ #Add functions from C extension to main LAPACK module
40
+ class << self
41
+ NMatrix::Internal::LAPACK.singleton_methods.each do |m|
42
+ define_method m, NMatrix::Internal::LAPACK.method(m).to_proc
43
+ end
44
+ end
45
+
46
+ class << self
47
+ # Solve the matrix equation AX = B, where A is a symmetric (or Hermitian)
48
+ # positive-definite matrix. If A is a nxn matrix, B must be mxn.
49
+ # Depending on the value of uplo, only the upper or lower half of +a+
50
+ # is read.
51
+ # This uses the Cholesky decomposition so it should be faster than
52
+ # the generic NMatrix#solve method.
53
+ # Doesn't modify inputs.
54
+ # Requires either the nmatrix-atlas or nmatrix-lapacke gem.
55
+ # * *Arguments* :
56
+ # - +uplo+ -> Either +:upper+ or +:lower+. Specifies which half of +a+ to read.
57
+ # - +a+ -> The matrix A.
58
+ # - +b+ -> The right-hand side B.
59
+ # * *Returns* :
60
+ # - The solution X
61
+ def posv(uplo, a, b)
62
+ raise(NotImplementedError, "Either the nmatrix-atlas or nmatrix-lapacke gem must be installed to use posv")
63
+ end
64
+
65
+ # laswp(matrix, ipiv) -> NMatrix
66
+ #
67
+ # Permute the columns of a matrix (in-place) according to the Array +ipiv+.
68
+ #
69
+ def laswp(matrix, ipiv)
70
+ raise(ArgumentError, "expected NMatrix for argument 0") unless matrix.is_a?(NMatrix)
71
+ raise(StorageTypeError, "LAPACK functions only work on :dense NMatrix instances") unless matrix.stype == :dense
72
+ raise(ArgumentError, "expected Array ipiv to have no more entries than NMatrix a has columns") if ipiv.size > matrix.shape[1]
73
+
74
+ clapack_laswp(matrix.shape[0], matrix, matrix.shape[1], 0, ipiv.size-1, ipiv, 1)
75
+ end
76
+
77
+ def alloc_svd_result(matrix)
78
+ [
79
+ NMatrix.new(matrix.shape[0], dtype: matrix.dtype),
80
+ NMatrix.new([[matrix.shape[0],matrix.shape[1]].min,1], dtype: matrix.abs_dtype),
81
+ NMatrix.new(matrix.shape[1], dtype: matrix.dtype)
82
+ ]
83
+ end
84
+
85
+
86
+ #
87
+ # call-seq:
88
+ # gesvd(matrix) -> [u, sigma, v_transpose]
89
+ # gesvd(matrix) -> [u, sigma, v_conjugate_transpose] # complex
90
+ #
91
+ # Compute the singular value decomposition of a matrix using LAPACK's GESVD function.
92
+ #
93
+ # Optionally accepts a +workspace_size+ parameter, which will be honored only if it is larger than what LAPACK
94
+ # requires.
95
+ #
96
+ # Requires either the nmatrix-lapacke or nmatrix-atlas gem.
97
+ #
98
+ def gesvd(matrix, workspace_size=1)
99
+ raise(NotImplementedError,"gesvd requires either the nmatrix-atlas or nmatrix-lapacke gem")
100
+ end
101
+
102
+ #
103
+ # call-seq:
104
+ # gesdd(matrix) -> [u, sigma, v_transpose]
105
+ # gesdd(matrix) -> [u, sigma, v_conjugate_transpose] # complex
106
+ #
107
+ # Compute the singular value decomposition of a matrix using LAPACK's GESDD function. This uses a divide-and-conquer
108
+ # strategy. See also #gesvd.
109
+ #
110
+ # Optionally accepts a +workspace_size+ parameter, which will be honored only if it is larger than what LAPACK
111
+ # requires.
112
+ #
113
+ # Requires either the nmatrix-lapacke or nmatrix-atlas gem.
114
+ #
115
+ def gesdd(matrix, workspace_size=nil)
116
+ raise(NotImplementedError,"gesvd requires either the nmatrix-atlas or nmatrix-lapacke gem")
117
+ end
118
+
119
+ #
120
+ # call-seq:
121
+ # geev(matrix) -> [eigenvalues, left_eigenvectors, right_eigenvectors]
122
+ # geev(matrix, :left) -> [eigenvalues, left_eigenvectors]
123
+ # geev(matrix, :right) -> [eigenvalues, right_eigenvectors]
124
+ #
125
+ # Perform eigenvalue decomposition on a matrix using LAPACK's xGEEV function.
126
+ #
127
+ # +eigenvalues+ is a n-by-1 NMatrix containing the eigenvalues.
128
+ #
129
+ # +right_eigenvalues+ is a n-by-n matrix such that its j'th column
130
+ # contains the (right) eigenvalue of +matrix+ corresponding
131
+ # to the j'th eigenvalue.
132
+ # This means that +matrix+ = RDR^(-1),
133
+ # where R is +right_eigenvalues+ and D is the diagonal matrix formed
134
+ # from +eigenvalues+.
135
+ #
136
+ # +left_eigenvalues+ is n-by-n and its columns are the left
137
+ # eigenvalues of +matrix+, using the {definition of left eigenvalue
138
+ # from LAPACK}[https://software.intel.com/en-us/node/521147].
139
+ #
140
+ # For real dtypes, +eigenvalues+ and the eigenvector matrices
141
+ # will be complex if and only if +matrix+ has complex eigenvalues.
142
+ #
143
+ # Only available if nmatrix-lapack or nmatrix-atlas is installed.
144
+ #
145
+ def geev(matrix, which=:both)
146
+ raise(NotImplementedError, "geev requires either the nmatrix-atlas or nmatrix-lapack gem")
147
+ end
148
+
149
+ # The following are functions that used to be implemented in C, but
150
+ # now require nmatrix-atlas to run properly, so we can just
151
+ # implemented their stubs in Ruby.
152
+ def lapack_gesvd(jobu, jobvt, m, n, a, lda, s, u, ldu, vt, ldvt, lwork)
153
+ raise(NotImplementedError,"lapack_gesvd requires the nmatrix-atlas gem")
154
+ end
155
+
156
+ def lapack_gesdd(jobz, m, n, a, lda, s, u, ldu, vt, ldvt, lwork)
157
+ raise(NotImplementedError,"lapack_gesdd requires the nmatrix-atlas gem")
158
+ end
159
+
160
+ def lapack_geev(jobvl, jobvr, n, a, lda, w, wi, vl, ldvl, vr, ldvr, lwork)
161
+ raise(NotImplementedError,"lapack_geev requires the nmatrix-atlas gem")
162
+ end
163
+
164
+ def clapack_potrf(order, uplo, n, a, lda)
165
+ raise(NotImplementedError,"clapack_potrf requires the nmatrix-atlas gem")
166
+ end
167
+
168
+ def clapack_potri(order, uplo, n, a, lda)
169
+ raise(NotImplementedError,"clapack_potri requires the nmatrix-atlas gem")
170
+ end
171
+
172
+ def clapack_potrs(order, uplo, n, nrhs, a, lda, b, ldb)
173
+ raise(NotImplementedError,"clapack_potrs requires the nmatrix-atlas gem")
174
+ end
175
+
176
+ def clapack_getri(order, n, a, lda, ipiv)
177
+ raise(NotImplementedError,"clapack_getri requires the nmatrix-atlas gem")
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,44 @@
1
+ #--
2
+ # = NMatrix
3
+ #
4
+ # A linear algebra library for scientific computation in Ruby.
5
+ # NMatrix is part of SciRuby.
6
+ #
7
+ # NMatrix was originally inspired by and derived from NArray, by
8
+ # Masahiro Tanaka: http://narray.rubyforge.org
9
+ #
10
+ # == Copyright Information
11
+ #
12
+ # SciRuby is Copyright (c) 2010 - 2014, Ruby Science Foundation
13
+ # NMatrix is Copyright (c) 2012 - 2014, John Woods and the Ruby Science Foundation
14
+ #
15
+ # Please see LICENSE.txt for additional copyright notices.
16
+ #
17
+ # == Contributing
18
+ #
19
+ # By contributing source code to SciRuby, you agree to be bound by
20
+ # our Contributor Agreement:
21
+ #
22
+ # * https://github.com/SciRuby/sciruby/wiki/Contributor-Agreement
23
+ #
24
+ # == lapack_plugin.rb
25
+ #
26
+ # This file `require`s either nmatrix-atlas or nmatrix-lapacke depending on which
27
+ # is available.
28
+ #
29
+ # The idea is that if a developer wants to use a LAPACK feature which is provided
30
+ # by both of these gems (e.g. NMatrix#potrf! or NMatrix::LAPACK.geev),
31
+ # but doesn't care which one is installed, they can
32
+ # just `require 'nmatrix/lapack_plugin'` rather than having to choose between
33
+ # `require 'nmatrix/lapacke'` or `require 'nmatrix/lapacke'`
34
+ #++
35
+
36
+ begin
37
+ require 'nmatrix/atlas'
38
+ rescue LoadError
39
+ begin
40
+ require 'nmatrix/lapacke'
41
+ rescue LoadError
42
+ raise(LoadError,"Either nmatrix-atlas or nmatrix-lapacke must be installed")
43
+ end
44
+ end
@@ -36,6 +36,31 @@ class NMatrix
36
36
  :asinh, :atanh, :exp, :log2, :log10, :sqrt, :cbrt, :erf, :erfc, :gamma, :-@]
37
37
  end
38
38
 
39
+ # Methods for generating permutation matrix from LU factorization results.
40
+ module FactorizeLUMethods
41
+ class << self
42
+ def permutation_matrix_from(pivot_array)
43
+ perm_arry = permutation_array_for(pivot_array)
44
+ n = NMatrix.zeros(perm_arry.size, dtype: :byte)
45
+
46
+ perm_arry.each_with_index { |e, i| n[e,i] = 1 }
47
+
48
+ n
49
+ end
50
+
51
+ def permutation_array_for(pivot_array)
52
+ perm_arry = Array.new(pivot_array.size) { |i| i }
53
+ perm_arry.each_index do |i|
54
+ #the pivot indices returned by LAPACK getrf are indexed starting
55
+ #from 1, so we need to subtract 1 here
56
+ perm_arry[i], perm_arry[pivot_array[i]-1] = perm_arry[pivot_array[i]-1], perm_arry[i]
57
+ end
58
+
59
+ perm_arry
60
+ end
61
+ end
62
+ end
63
+
39
64
  #
40
65
  # call-seq:
41
66
  # invert! -> NMatrix
@@ -44,23 +69,18 @@ class NMatrix
44
69
  # Only works on dense matrices. Alternatively uses in-place Gauss-Jordan
45
70
  # elimination.
46
71
  #
72
+ # * *Raises* :
73
+ # - +StorageTypeError+ -> only implemented on dense matrices.
74
+ # - +ShapeError+ -> matrix must be square.
75
+ # - +DataTypeError+ -> cannot invert an integer matrix in-place.
76
+ #
47
77
  def invert!
48
- if NMatrix.has_clapack?
49
- # Get the pivot array; factor the matrix
50
- pivot = self.getrf!
51
-
52
- # Now calculate the inverse using the pivot array
53
- NMatrix::LAPACK::clapack_getri(:row, self.shape[1], self, self.shape[1], pivot)
78
+ raise(StorageTypeError, "invert only works on dense matrices currently") unless self.dense?
79
+ raise(ShapeError, "Cannot invert non-square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
80
+ raise(DataTypeError, "Cannot invert an integer matrix in-place") if self.integer_dtype?
54
81
 
55
- self
56
- else
57
- if self.integer_dtype?
58
- __inverse__(self.cast(dtype: :rational128), true)
59
- else
60
- dtype = self.dtype
61
- __inverse__(self, true)
62
- end
63
- end
82
+ #No internal implementation of getri, so use this other function
83
+ __inverse__(self, true)
64
84
  end
65
85
 
66
86
  #
@@ -70,45 +90,42 @@ class NMatrix
70
90
  # Make a copy of the matrix, then invert using Gauss-Jordan elimination.
71
91
  # Works without LAPACK.
72
92
  #
73
- #
74
93
  # * *Returns* :
75
- # - A dense NMatrix.
76
- #
77
- def invert lda=nil, ldb=nil
78
- if lda.nil? and ldb.nil?
79
- if NMatrix.has_clapack?
80
- begin
81
- self.cast(:dense, self.dtype).invert! # call CLAPACK version
82
- rescue NotImplementedError # probably a rational matrix
83
- inverse = self.clone
84
- __inverse__(inverse, false)
85
- end
86
- elsif self.integer_dtype? # FIXME: This check is probably too slow.
87
- rational_self = self.cast(dtype: :rational128)
88
- inverse = rational_self.clone
89
- rational_self.__inverse__(inverse, false)
90
- else
91
- inverse = self.clone
92
- __inverse__(inverse, false)
93
- end
94
+ # - A dense NMatrix. Will be the same type as the input NMatrix,
95
+ # except if the input is an integral dtype, in which case it will be a
96
+ # :float64 NMatrix.
97
+ #
98
+ # * *Raises* :
99
+ # - +StorageTypeError+ -> only implemented on dense matrices.
100
+ # - +ShapeError+ -> matrix must be square.
101
+ #
102
+ def invert
103
+ #write this in terms of invert! so plugins will only have to overwrite
104
+ #invert! and not invert
105
+ if self.integer_dtype?
106
+ cloned = self.cast(dtype: :float64)
107
+ cloned.invert!
94
108
  else
95
- inverse = self.clone_structure
96
- if self.integer_dtype?
97
- __inverse_exact__(inverse.cast(dtype: :rational128), lda, ldb)
98
- else
99
- dtype = self.dtype
100
- __inverse_exact__(inverse, lda, ldb)
101
- end
109
+ cloned = self.clone
110
+ cloned.invert!
102
111
  end
103
112
  end
104
113
  alias :inverse :invert
105
114
 
106
115
  #
107
116
  # call-seq:
108
- # getrf! -> NMatrix
117
+ # getrf! -> Array
109
118
  #
110
119
  # LU factorization of a general M-by-N matrix +A+ using partial pivoting with
111
- # row interchanges. Only works in dense matrices.
120
+ # row interchanges. The LU factorization is A = PLU, where P is a row permutation
121
+ # matrix, L is a lower triangular matrix with unit diagonals, and U is an upper
122
+ # triangular matrix (note that this convention is different from the
123
+ # clapack_getrf behavior, but matches the standard LAPACK getrf).
124
+ # +A+ is overwritten with the elements of L and U (the unit
125
+ # diagonal elements of L are not saved). P is not returned directly and must be
126
+ # constructed from the pivot array ipiv. The row indices in ipiv are indexed
127
+ # starting from 1.
128
+ # Only works for dense matrices.
112
129
  #
113
130
  # * *Returns* :
114
131
  # - The IPIV vector. The L and U matrices are stored in A.
@@ -117,44 +134,51 @@ class NMatrix
117
134
  #
118
135
  def getrf!
119
136
  raise(StorageTypeError, "ATLAS functions only work on dense matrices") unless self.dense?
120
- NMatrix::LAPACK::clapack_getrf(:row, self.shape[0], self.shape[1], self, self.shape[1])
121
- end
122
-
123
137
 
124
- #
125
- # call-seq:
126
- # getrf -> NMatrix
127
- #
128
- # In-place version of #getrf!. Returns the new matrix, which contains L and U matrices.
129
- #
130
- # * *Raises* :
131
- # - +StorageTypeError+ -> ATLAS functions only work on dense matrices.
132
- #
133
- def getrf
134
- a = self.clone
135
- a.getrf!
136
- return a
138
+ #For row-major matrices, clapack_getrf uses a different convention than
139
+ #described above (U has unit diagonal elements instead of L and columns
140
+ #are interchanged rather than rows). For column-major matrices, clapack
141
+ #uses the stanard conventions. So we just transpose the matrix before
142
+ #and after calling clapack_getrf.
143
+ #Unfortunately, this is not a very good way, uses a lot of memory.
144
+ temp = self.transpose
145
+ ipiv = NMatrix::LAPACK::clapack_getrf(:col, self.shape[0], self.shape[1], temp, self.shape[0])
146
+ temp = temp.transpose
147
+ self[0...self.shape[0], 0...self.shape[1]] = temp
148
+
149
+ #for some reason, in clapack_getrf, the indices in ipiv start from 0
150
+ #instead of 1 as in LAPACK.
151
+ ipiv.each_index { |i| ipiv[i]+=1 }
152
+
153
+ return ipiv
137
154
  end
138
155
 
139
-
140
156
  #
141
157
  # call-seq:
142
158
  # potrf!(upper_or_lower) -> NMatrix
143
159
  #
144
160
  # Cholesky factorization of a symmetric positive-definite matrix -- or, if complex,
145
- # a Hermitian positive-definite matrix +A+. This uses the ATLAS function clapack_potrf,
146
- # so the result will be written in either the upper or lower triangular portion of the
147
- # matrix upon which it is called.
161
+ # a Hermitian positive-definite matrix +A+.
162
+ # The result will be written in either the upper or lower triangular portion of the
163
+ # matrix, depending on whether the argument is +:upper+ or +:lower+.
164
+ # Also the function only reads in the upper or lower part of the matrix,
165
+ # so it doesn't actually have to be symmetric/Hermitian.
166
+ # However, if the matrix (i.e. the symmetric matrix implied by the lower/upper
167
+ # half) is not positive-definite, the function will return nonsense.
168
+ #
169
+ # This functions requires either the nmatrix-atlas or nmatrix-lapacke gem
170
+ # installed.
148
171
  #
149
172
  # * *Returns* :
150
173
  # the triangular portion specified by the parameter
151
174
  # * *Raises* :
152
175
  # - +StorageTypeError+ -> ATLAS functions only work on dense matrices.
176
+ # - +ShapeError+ -> Must be square.
177
+ # - +NotImplementedError+ -> If called without nmatrix-atlas or nmatrix-lapacke gem
153
178
  #
154
179
  def potrf!(which)
155
- raise(StorageTypeError, "ATLAS functions only work on dense matrices") unless self.dense?
156
- # FIXME: Surely there's an easy way to calculate one of these from the other. Do we really need to run twice?
157
- NMatrix::LAPACK::clapack_potrf(:row, which, self.shape[0], self, self.shape[1])
180
+ # The real implementation is in the plugin files.
181
+ raise(NotImplementedError, "potrf! requires either the nmatrix-atlas or nmatrix-lapacke gem")
158
182
  end
159
183
 
160
184
  def potrf_upper!
@@ -168,30 +192,102 @@ class NMatrix
168
192
 
169
193
  #
170
194
  # call-seq:
171
- # factorize_cholesky -> ...
195
+ # factorize_cholesky -> [upper NMatrix, lower NMatrix]
172
196
  #
173
- # Cholesky factorization of a matrix.
197
+ # Calculates the Cholesky factorization of a matrix and returns the
198
+ # upper and lower matrices such that A=LU and L=U*, where * is
199
+ # either the transpose or conjugate transpose.
200
+ #
201
+ # Unlike potrf!, this makes method requires that the original is matrix is
202
+ # symmetric or Hermitian. However, it is still your responsibility to make
203
+ # sure it is positive-definite.
174
204
  def factorize_cholesky
175
- [self.clone.potrf_upper!.triu!,
176
- self.clone.potrf_lower!.tril!]
205
+ raise "Matrix must be symmetric/Hermitian for Cholesky factorization" unless self.hermitian?
206
+ l = self.clone.potrf_lower!.tril!
207
+ u = l.conjugate_transpose
208
+ [u,l]
177
209
  end
178
210
 
179
211
  #
180
212
  # call-seq:
181
213
  # factorize_lu -> ...
182
214
  #
183
- # LU factorization of a matrix.
184
- #
185
- # FIXME: For some reason, getrf seems to require that the matrix be transposed first -- and then you have to transpose the
186
- # FIXME: result again. Ideally, this would be an in-place factorize instead, and would be called nm_factorize_lu_bang.
187
- #
188
- def factorize_lu
215
+ # LU factorization of a matrix. Optionally return the permutation matrix.
216
+ # Note that computing the permutation matrix will introduce a slight memory
217
+ # and time overhead.
218
+ #
219
+ # == Arguments
220
+ #
221
+ # +with_permutation_matrix+ - If set to *true* will return the permutation
222
+ # matrix alongwith the LU factorization as a second return value.
223
+ #
224
+ def factorize_lu with_permutation_matrix=nil
189
225
  raise(NotImplementedError, "only implemented for dense storage") unless self.stype == :dense
190
226
  raise(NotImplementedError, "matrix is not 2-dimensional") unless self.dimensions == 2
191
227
 
192
- t = self.transpose
193
- NMatrix::LAPACK::clapack_getrf(:row, t.shape[0], t.shape[1], t, t.shape[1])
194
- t.transpose
228
+ t = self.clone
229
+ pivot = t.getrf!
230
+ return t unless with_permutation_matrix
231
+
232
+ [t, FactorizeLUMethods.permutation_matrix_from(pivot)]
233
+ end
234
+
235
+ # Reduce self to upper hessenberg form using householder transforms.
236
+ #
237
+ # == References
238
+ #
239
+ # * http://en.wikipedia.org/wiki/Hessenberg_matrix
240
+ # * http://www.mymathlib.com/c_source/matrices/eigen/hessenberg_orthog.c
241
+ def hessenberg
242
+ clone.hessenberg!
243
+ end
244
+
245
+ # Destructive version of #hessenberg
246
+ def hessenberg!
247
+ raise ShapeError, "Trying to reduce non 2D matrix to hessenberg form" if
248
+ shape.size != 2
249
+ raise ShapeError, "Trying to reduce non-square matrix to hessenberg form" if
250
+ shape[0] != shape[1]
251
+ raise StorageTypeError, "Matrix must be dense" if stype != :dense
252
+ raise TypeError, "Works with float matrices only" unless
253
+ [:float64,:float32].include?(dtype)
254
+
255
+ __hessenberg__(self)
256
+ self
257
+ end
258
+
259
+ # Solve the matrix equation AX = B, where A is +self+, B is the first
260
+ # argument, and X is returned. A must be a nxn square matrix, while B must be
261
+ # nxm. Only works with dense
262
+ # matrices and non-integer, non-object data types.
263
+ #
264
+ # == Usage
265
+ #
266
+ # a = NMatrix.new [2,2], [3,1,1,2], dtype: dtype
267
+ # b = NMatrix.new [2,1], [9,8], dtype: dtype
268
+ # a.solve(b)
269
+ def solve b
270
+ raise(ShapeError, "Must be called on square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
271
+ raise(ShapeError, "number of rows of b must equal number of cols of self") if
272
+ self.shape[1] != b.shape[0]
273
+ raise ArgumentError, "only works with dense matrices" if self.stype != :dense
274
+ raise ArgumentError, "only works for non-integer, non-object dtypes" if
275
+ integer_dtype? or object_dtype? or b.integer_dtype? or b.object_dtype?
276
+
277
+ x = b.clone
278
+ clone = self.clone
279
+ n = self.shape[0]
280
+ nrhs = b.shape[1]
281
+
282
+ ipiv = NMatrix::LAPACK.clapack_getrf(:row, n, n, clone, n)
283
+ # When we call clapack_getrs with :row, actually only the first matrix
284
+ # (i.e. clone) is interpreted as row-major, while the other matrix (x)
285
+ # is interpreted as column-major. See here: http://math-atlas.sourceforge.net/faq.html#RowSolve
286
+ # So we must transpose x before and after
287
+ # calling it.
288
+ x = x.transpose
289
+ NMatrix::LAPACK.clapack_getrs(:row, :no_transpose, n, nrhs, clone, n, ipiv, x, n)
290
+ x.transpose
195
291
  end
196
292
 
197
293
  #
@@ -254,24 +350,80 @@ class NMatrix
254
350
  def gesdd(workspace_size=nil)
255
351
  self.clone.gesdd!(workspace_size)
256
352
  end
353
+
257
354
  #
258
355
  # call-seq:
259
356
  # laswp!(ary) -> NMatrix
260
357
  #
261
- # In-place permute the columns of a dense matrix using LASWP according to the order given in an Array +ary+.
262
- # Not yet implemented for yale or list.
263
- def laswp!(ary)
264
- NMatrix::LAPACK::laswp(self, ary)
358
+ # In-place permute the columns of a dense matrix using LASWP according to the order given as an array +ary+.
359
+ #
360
+ # If +:convention+ is +:lapack+, then +ary+ represents a sequence of pair-wise permutations which are
361
+ # performed successively. That is, the i'th entry of +ary+ is the index of the column to swap
362
+ # the i'th column with, having already applied all earlier swaps.
363
+ #
364
+ # If +:convention+ is +:intuitive+, then +ary+ represents the order of columns after the permutation.
365
+ # That is, the i'th entry of +ary+ is the index of the column that will be in position i after the
366
+ # reordering (Matlab-like behaviour). This is the default.
367
+ #
368
+ # Not yet implemented for yale or list.
369
+ #
370
+ # == Arguments
371
+ #
372
+ # * +ary+ - An Array specifying the order of the columns. See above for details.
373
+ #
374
+ # == Options
375
+ #
376
+ # * +:covention+ - Possible values are +:lapack+ and +:intuitive+. Default is +:intuitive+. See above for details.
377
+ #
378
+ def laswp!(ary, opts={})
379
+ raise(StorageTypeError, "ATLAS functions only work on dense matrices") unless self.dense?
380
+ opts = { convention: :intuitive }.merge(opts)
381
+
382
+ if opts[:convention] == :intuitive
383
+ if ary.length != ary.uniq.length
384
+ raise(ArgumentError, "No duplicated entries in the order array are allowed under convention :intuitive")
385
+ end
386
+ n = self.shape[1]
387
+ p = []
388
+ order = (0...n).to_a
389
+ 0.upto(n-2) do |i|
390
+ p[i] = order.index(ary[i])
391
+ order[i], order[p[i]] = order[p[i]], order[i]
392
+ end
393
+ p[n-1] = n-1
394
+ else
395
+ p = ary
396
+ end
397
+
398
+ NMatrix::LAPACK::laswp(self, p)
265
399
  end
266
400
 
267
401
  #
268
402
  # call-seq:
269
403
  # laswp(ary) -> NMatrix
270
404
  #
271
- # Permute the columns of a dense matrix using LASWP according to the order given in an Array +ary+.
272
- # Not yet implemented for yale or list.
273
- def laswp(ary)
274
- self.clone.laswp!(ary)
405
+ # Permute the columns of a dense matrix using LASWP according to the order given in an array +ary+.
406
+ #
407
+ # If +:convention+ is +:lapack+, then +ary+ represents a sequence of pair-wise permutations which are
408
+ # performed successively. That is, the i'th entry of +ary+ is the index of the column to swap
409
+ # the i'th column with, having already applied all earlier swaps. This is the default.
410
+ #
411
+ # If +:convention+ is +:intuitive+, then +ary+ represents the order of columns after the permutation.
412
+ # That is, the i'th entry of +ary+ is the index of the column that will be in position i after the
413
+ # reordering (Matlab-like behaviour).
414
+ #
415
+ # Not yet implemented for yale or list.
416
+ #
417
+ # == Arguments
418
+ #
419
+ # * +ary+ - An Array specifying the order of the columns. See above for details.
420
+ #
421
+ # == Options
422
+ #
423
+ # * +:covention+ - Possible values are +:lapack+ and +:intuitive+. Default is +:lapack+. See above for details.
424
+ #
425
+ def laswp(ary, opts={})
426
+ self.clone.laswp!(ary, opts)
275
427
  end
276
428
 
277
429
  #
@@ -279,7 +431,7 @@ class NMatrix
279
431
  # det -> determinant
280
432
  #
281
433
  # Calculate the determinant by way of LU decomposition. This is accomplished
282
- # using clapack_getrf, and then by summing the diagonal elements. There is a
434
+ # using clapack_getrf, and then by taking the product of the diagonal elements. There is a
283
435
  # risk of underflow/overflow.
284
436
  #
285
437
  # There are probably also more efficient ways to calculate the determinant.
@@ -290,35 +442,38 @@ class NMatrix
290
442
  #
291
443
  # This function is guaranteed to return the same type of data in the matrix
292
444
  # upon which it is called.
293
- # In other words, if you call it on a rational matrix, you'll get a rational
294
- # number back.
295
445
  #
296
- # Integer matrices are converted to rational matrices for the purposes of
446
+ # Integer matrices are converted to floating point matrices for the purposes of
297
447
  # performing the calculation, as xGETRF can't work on integer matrices.
298
448
  #
299
449
  # * *Returns* :
300
450
  # - The determinant of the matrix. It's the same type as the matrix's dtype.
301
451
  # * *Raises* :
302
- # - +NotImplementedError+ -> Must be used in 2D matrices.
452
+ # - +ShapeError+ -> Must be used on square matrices.
303
453
  #
304
454
  def det
305
- raise(NotImplementedError, "determinant can be calculated only for 2D matrices") unless self.dim == 2
455
+ raise(ShapeError, "determinant can be calculated only for square matrices") unless self.dim == 2 && self.shape[0] == self.shape[1]
306
456
 
307
457
  # Cast to a dtype for which getrf is implemented
308
- new_dtype = [:byte,:int8,:int16,:int32,:int64].include?(self.dtype) ? :rational128 : self.dtype
458
+ new_dtype = self.integer_dtype? ? :float64 : self.dtype
309
459
  copy = self.cast(:dense, new_dtype)
310
460
 
311
461
  # Need to know the number of permutations. We'll add up the diagonals of
312
462
  # the factorized matrix.
313
463
  pivot = copy.getrf!
314
464
 
315
- prod = pivot.size % 2 == 1 ? -1 : 1 # odd permutations => negative
465
+ num_perm = 0 #number of permutations
466
+ pivot.each_with_index do |swap, i|
467
+ #pivot indexes rows starting from 1, instead of 0, so need to subtract 1 here
468
+ num_perm += 1 if swap-1 != i
469
+ end
470
+ prod = num_perm % 2 == 1 ? -1 : 1 # odd permutations => negative
316
471
  [shape[0],shape[1]].min.times do |i|
317
472
  prod *= copy[i,i]
318
473
  end
319
474
 
320
475
  # Convert back to an integer if necessary
321
- new_dtype != self.dtype ? prod.to_i : prod
476
+ new_dtype != self.dtype ? prod.round : prod #prevent rounding errors
322
477
  end
323
478
 
324
479
  #
@@ -342,6 +497,120 @@ class NMatrix
342
497
  self.cast(new_stype, NMatrix::upcast(dtype, :complex64)).complex_conjugate!
343
498
  end
344
499
 
500
+ # Calculate the variance co-variance matrix
501
+ #
502
+ # == Options
503
+ #
504
+ # * +:for_sample_data+ - Default true. If set to false will consider the denominator for
505
+ # population data (i.e. N, as opposed to N-1 for sample data).
506
+ #
507
+ # == References
508
+ #
509
+ # * http://stattrek.com/matrix-algebra/covariance-matrix.aspx
510
+ def cov(opts={})
511
+ raise TypeError, "Only works for non-integer dtypes" if integer_dtype?
512
+ opts = {
513
+ for_sample_data: true
514
+ }.merge(opts)
515
+
516
+ denominator = opts[:for_sample_data] ? rows - 1 : rows
517
+ ones = NMatrix.ones [rows,1]
518
+ deviation_scores = self - ones.dot(ones.transpose).dot(self) / rows
519
+ deviation_scores.transpose.dot(deviation_scores) / denominator
520
+ end
521
+
522
+ # Calculate the correlation matrix.
523
+ def corr
524
+ raise NotImplementedError, "Does not work for complex dtypes" if complex_dtype?
525
+ standard_deviation = std
526
+ cov / (standard_deviation.transpose.dot(standard_deviation))
527
+ end
528
+
529
+ # Raise a square matrix to a power. Be careful of numeric overflows!
530
+ # In case *n* is 0, an identity matrix of the same dimension is returned. In case
531
+ # of negative *n*, the matrix is inverted and the absolute value of *n* taken
532
+ # for computing the power.
533
+ #
534
+ # == Arguments
535
+ #
536
+ # * +n+ - Integer to which self is to be raised.
537
+ #
538
+ # == References
539
+ #
540
+ # * R.G Dromey - How to Solve it by Computer. Link -
541
+ # http://www.amazon.com/Solve-Computer-Prentice-Hall-International-Science/dp/0134340019/ref=sr_1_1?ie=UTF8&qid=1422605572&sr=8-1&keywords=how+to+solve+it+by+computer
542
+ def pow n
543
+ raise ShapeError, "Only works with 2D square matrices." if
544
+ shape[0] != shape[1] or shape.size != 2
545
+ raise TypeError, "Only works with integer powers" unless n.is_a?(Integer)
546
+
547
+ sequence = (integer_dtype? ? self.cast(dtype: :int64) : self).clone
548
+ product = NMatrix.eye shape[0], dtype: sequence.dtype, stype: sequence.stype
549
+
550
+ if n == 0
551
+ return NMatrix.eye(shape, dtype: dtype, stype: stype)
552
+ elsif n == 1
553
+ return sequence
554
+ elsif n < 0
555
+ n = n.abs
556
+ sequence.invert!
557
+ product = NMatrix.eye shape[0], dtype: sequence.dtype, stype: sequence.stype
558
+ end
559
+
560
+ # Decompose n to reduce the number of multiplications.
561
+ while n > 0
562
+ product = product.dot(sequence) if n % 2 == 1
563
+ n = n / 2
564
+ sequence = sequence.dot(sequence)
565
+ end
566
+
567
+ product
568
+ end
569
+
570
+ # Compute the Kronecker product of +self+ and other NMatrix
571
+ #
572
+ # === Arguments
573
+ #
574
+ # * +mat+ - A 2D NMatrix object
575
+ #
576
+ # === Usage
577
+ #
578
+ # a = NMatrix.new([2,2],[1,2,
579
+ # 3,4])
580
+ # b = NMatrix.new([2,3],[1,1,1,
581
+ # 1,1,1], dtype: :float64)
582
+ # a.kron_prod(b) # => [ [1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
583
+ # [1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
584
+ # [3.0, 3.0, 3.0, 4.0, 4.0, 4.0]
585
+ # [3.0, 3.0, 3.0, 4.0, 4.0, 4.0] ]
586
+ #
587
+ def kron_prod(mat)
588
+ unless self.dimensions==2 and mat.dimensions==2
589
+ raise ShapeError, "Implemented for 2D NMatrix objects only."
590
+ end
591
+
592
+ # compute the shape [n,m] of the product matrix
593
+ n, m = self.shape[0]*mat.shape[0], self.shape[1]*mat.shape[1]
594
+ # compute the entries of the product matrix
595
+ kron_prod_array = []
596
+ if self.yale?
597
+ # +:yale+ requires to get the row by copy in order to apply +#transpose+ to it
598
+ self.each_row(getby=:copy) do |selfr|
599
+ mat.each_row do |matr|
600
+ kron_prod_array += (selfr.transpose.dot matr).to_flat_a
601
+ end
602
+ end
603
+ else
604
+ self.each_row do |selfr|
605
+ mat.each_row do |matr|
606
+ kron_prod_array += (selfr.transpose.dot matr).to_flat_a
607
+ end
608
+ end
609
+ end
610
+
611
+ NMatrix.new([n,m], kron_prod_array)
612
+ end
613
+
345
614
  #
346
615
  # call-seq:
347
616
  # conjugate_transpose -> NMatrix
@@ -356,27 +625,6 @@ class NMatrix
356
625
  self.transpose.complex_conjugate!
357
626
  end
358
627
 
359
- #
360
- # call-seq:
361
- # hermitian? -> Boolean
362
- #
363
- # A hermitian matrix is a complex square matrix that is equal to its
364
- # conjugate transpose. (http://en.wikipedia.org/wiki/Hermitian_matrix)
365
- #
366
- # * *Returns* :
367
- # - True if +self+ is a hermitian matrix, nil otherwise.
368
- #
369
- def hermitian?
370
- return false if self.dim != 2 or self.shape[0] != self.shape[1]
371
-
372
- if [:complex64, :complex128].include?(self.dtype)
373
- # TODO: Write much faster Hermitian test in C
374
- self.eql?(self.conjugate_transpose)
375
- else
376
- symmetric?
377
- end
378
- end
379
-
380
628
  #
381
629
  # call-seq:
382
630
  # trace -> Numeric
@@ -555,7 +803,10 @@ class NMatrix
555
803
  #
556
804
  # Return the sum of the contents of the vector. This is the BLAS asum routine.
557
805
  def asum incx=1, n=nil
558
- return self[0].abs if self.shape == [1]
806
+ if self.shape == [1]
807
+ return self[0].abs unless self.complex_dtype?
808
+ return self[0].real.abs + self[0].imag.abs
809
+ end
559
810
  return method_missing(:asum, incx, n) unless vector?
560
811
  NMatrix::BLAS::asum(self, incx, self.size / incx)
561
812
  end
@@ -608,7 +859,7 @@ protected
608
859
  # These don't actually take an argument -- they're called reverse-polish style on the matrix.
609
860
  # This group always gets casted to float64.
610
861
  [:log, :log2, :log10, :sqrt, :sin, :cos, :tan, :acos, :asin, :atan, :cosh, :sinh, :tanh, :acosh,
611
- :asinh, :atanh, :exp, :erf, :erfc, :gamma, :cbrt].each do |ewop|
862
+ :asinh, :atanh, :exp, :erf, :erfc, :gamma, :cbrt, :round].each do |ewop|
612
863
  define_method("__list_unary_#{ewop}__") do
613
864
  self.__list_map_stored__(nil) { |l| Math.send(ewop, l) }.cast(stype, NMatrix.upcast(dtype, :float64))
614
865
  end
@@ -648,31 +899,31 @@ protected
648
899
  end
649
900
  #:startdoc:
650
901
 
651
- # These are for rounding each value of a matrix
652
- def __list_unary_round__
902
+ # These are for rounding each value of a matrix. Takes an optional argument
903
+ def __list_unary_round__(precision)
653
904
  if self.complex_dtype?
654
- self.__list_map_stored__(nil) { |l| Complex(l.real.round, l.imag.round) }
905
+ self.__list_map_stored__(nil) { |l| Complex(l.real.round(precision), l.imag.round(precision)) }
655
906
  .cast(stype, dtype)
656
907
  else
657
- self.__list_map_stored__(nil) { |l| l.round }.cast(stype, dtype)
908
+ self.__list_map_stored__(nil) { |l| l.round(precision) }.cast(stype, dtype)
658
909
  end
659
910
  end
660
911
 
661
- def __yale_unary_round__
912
+ def __yale_unary_round__(precision)
662
913
  if self.complex_dtype?
663
- self.__yale_map_stored__ { |l| Complex(l.real.round, l.imag.round) }
914
+ self.__yale_map_stored__ { |l| Complex(l.real.round(precision), l.imag.round(precision)) }
664
915
  .cast(stype, dtype)
665
916
  else
666
- self.__yale_map_stored__ { |l| l.round }.cast(stype, dtype)
917
+ self.__yale_map_stored__ { |l| l.round(precision) }.cast(stype, dtype)
667
918
  end
668
919
  end
669
920
 
670
- def __dense_unary_round__
921
+ def __dense_unary_round__(precision)
671
922
  if self.complex_dtype?
672
- self.__dense_map__ { |l| Complex(l.real.round, l.imag.round) }
923
+ self.__dense_map__ { |l| Complex(l.real.round(precision), l.imag.round(precision)) }
673
924
  .cast(stype, dtype)
674
925
  else
675
- self.__dense_map__ { |l| l.round }.cast(stype, dtype)
926
+ self.__dense_map__ { |l| l.round(precision) }.cast(stype, dtype)
676
927
  end
677
928
  end
678
929
 
@@ -680,7 +931,7 @@ protected
680
931
  def dtype_for_floor_or_ceil
681
932
  if self.integer_dtype? or [:complex64, :complex128, :object].include?(self.dtype)
682
933
  return_dtype = dtype
683
- elsif [:float32, :float64, :rational32,:rational64, :rational128].include?(self.dtype)
934
+ elsif [:float32, :float64].include?(self.dtype)
684
935
  return_dtype = :int64
685
936
  end
686
937