nmatrix 0.2.3 → 0.2.4

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.
@@ -65,8 +65,8 @@ class NMatrix
65
65
  # call-seq:
66
66
  # invert! -> NMatrix
67
67
  #
68
- # Use LAPACK to calculate the inverse of the matrix (in-place) if available.
69
- # Only works on dense matrices. Alternatively uses in-place Gauss-Jordan
68
+ # Use LAPACK to calculate the inverse of the matrix (in-place) if available.
69
+ # Only works on dense matrices. Alternatively uses in-place Gauss-Jordan
70
70
  # elimination.
71
71
  #
72
72
  # * *Raises* :
@@ -112,315 +112,174 @@ class NMatrix
112
112
  end
113
113
  alias :inverse :invert
114
114
 
115
- #
116
115
  # call-seq:
117
- # adjugate! -> NMatrix
116
+ # exact_inverse! -> NMatrix
118
117
  #
119
- # Calculate the adjugate of the matrix (in-place).
120
- # Only works on dense matrices.
118
+ # Calulates inverse_exact of a matrix of size 2 or 3.
119
+ # Only works on dense matrices.
121
120
  #
122
121
  # * *Raises* :
123
- # - +StorageTypeError+ -> only implemented on dense matrices.
124
- # - +ShapeError+ -> matrix must be square.
125
- # - +DataTypeError+ -> cannot calculate adjugate of an integer matrix in-place.
126
- #
127
- def adjugate!
128
- raise(StorageTypeError, "adjugate only works on dense matrices currently") unless self.dense?
129
- raise(ShapeError, "Cannot calculate adjugate of a non-square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
130
- raise(DataTypeError, "Cannot calculate adjugate of an integer matrix in-place") if self.integer_dtype?
131
- d = self.det
132
- self.invert!
133
- self.map! { |e| e * d }
134
- self
122
+ # - +DataTypeError+ -> cannot invert an integer matrix in-place.
123
+ # - +NotImplementedError+ -> cannot find exact inverse of matrix with size greater than 3 #
124
+ def exact_inverse!
125
+ raise(ShapeError, "Cannot invert non-square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
126
+ raise(DataTypeError, "Cannot invert an integer matrix in-place") if self.integer_dtype?
127
+ #No internal implementation of getri, so use this other function
128
+ n = self.shape[0]
129
+ if n>3
130
+ raise(NotImplementedError, "Cannot find exact inverse of matrix of size greater than 3")
131
+ else
132
+ clond=self.clone
133
+ __inverse_exact__(clond, n, n)
134
+ end
135
135
  end
136
- alias :adjoint! :adjugate!
137
136
 
138
137
  #
139
138
  # call-seq:
140
- # adjugate -> NMatrix
139
+ # exact_inverse -> NMatrix
141
140
  #
142
- # Make a copy of the matrix and calculate the adjugate of the matrix.
143
- # Only works on dense matrices.
141
+ # Make a copy of the matrix, then invert using exact_inverse
144
142
  #
145
143
  # * *Returns* :
146
144
  # - A dense NMatrix. Will be the same type as the input NMatrix,
147
145
  # except if the input is an integral dtype, in which case it will be a
148
- # :float64 NMatrix.
146
+ # :float64 NMatrix.
149
147
  #
150
148
  # * *Raises* :
151
149
  # - +StorageTypeError+ -> only implemented on dense matrices.
152
150
  # - +ShapeError+ -> matrix must be square.
151
+ # - +NotImplementedError+ -> cannot find exact inverse of matrix with size greater than 3
153
152
  #
154
- def adjugate
155
- raise(StorageTypeError, "adjugate only works on dense matrices currently") unless self.dense?
156
- raise(ShapeError, "Cannot calculate adjugate of a non-square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
157
- d = self.det
158
- mat = self.invert
159
- mat.map! { |e| e * d }
160
- mat
153
+ def exact_inverse
154
+ #write this in terms of exact_inverse! so plugins will only have to overwrite
155
+ #exact_inverse! and not exact_inverse
156
+ if self.integer_dtype?
157
+ cloned = self.cast(dtype: :float64)
158
+ cloned.exact_inverse!
159
+ else
160
+ cloned = self.clone
161
+ cloned.exact_inverse!
162
+ end
161
163
  end
162
- alias :adjoint :adjugate
164
+ alias :invert_exactly :exact_inverse
163
165
 
164
- #
165
- # call-seq:
166
- # getrf! -> Array
167
- #
168
- # LU factorization of a general M-by-N matrix +A+ using partial pivoting with
169
- # row interchanges. The LU factorization is A = PLU, where P is a row permutation
170
- # matrix, L is a lower triangular matrix with unit diagonals, and U is an upper
171
- # triangular matrix (note that this convention is different from the
172
- # clapack_getrf behavior, but matches the standard LAPACK getrf).
173
- # +A+ is overwritten with the elements of L and U (the unit
174
- # diagonal elements of L are not saved). P is not returned directly and must be
175
- # constructed from the pivot array ipiv. The row indices in ipiv are indexed
176
- # starting from 1.
177
- # Only works for dense matrices.
178
- #
179
- # * *Returns* :
180
- # - The IPIV vector. The L and U matrices are stored in A.
181
- # * *Raises* :
182
- # - +StorageTypeError+ -> ATLAS functions only work on dense matrices.
183
- #
184
- def getrf!
185
- raise(StorageTypeError, "ATLAS functions only work on dense matrices") unless self.dense?
186
-
187
- #For row-major matrices, clapack_getrf uses a different convention than
188
- #described above (U has unit diagonal elements instead of L and columns
189
- #are interchanged rather than rows). For column-major matrices, clapack
190
- #uses the stanard conventions. So we just transpose the matrix before
191
- #and after calling clapack_getrf.
192
- #Unfortunately, this is not a very good way, uses a lot of memory.
193
- temp = self.transpose
194
- ipiv = NMatrix::LAPACK::clapack_getrf(:col, self.shape[0], self.shape[1], temp, self.shape[0])
195
- temp = temp.transpose
196
- self[0...self.shape[0], 0...self.shape[1]] = temp
197
-
198
- #for some reason, in clapack_getrf, the indices in ipiv start from 0
199
- #instead of 1 as in LAPACK.
200
- ipiv.each_index { |i| ipiv[i]+=1 }
201
-
202
- return ipiv
203
- end
204
166
 
205
- #
206
- # call-seq:
207
- # geqrf! -> shape.min x 1 NMatrix
208
- #
209
- # QR factorization of a general M-by-N matrix +A+.
210
- #
211
- # The QR factorization is A = QR, where Q is orthogonal and R is Upper Triangular
212
- # +A+ is overwritten with the elements of R and Q with Q being represented by the
213
- # elements below A's diagonal and an array of scalar factors in the output NMatrix.
214
- #
215
- # The matrix Q is represented as a product of elementary reflectors
216
- # Q = H(1) H(2) . . . H(k), where k = min(m,n).
217
- #
218
- # Each H(i) has the form
219
- #
220
- # H(i) = I - tau * v * v'
221
- #
222
- # http://www.netlib.org/lapack/explore-html/d3/d69/dgeqrf_8f.html
223
- #
224
- # Only works for dense matrices.
225
- #
226
- # * *Returns* :
227
- # - Vector TAU. Q and R are stored in A. Q is represented by TAU and A
228
- # * *Raises* :
229
- # - +StorageTypeError+ -> LAPACK functions only work on dense matrices.
230
- #
231
- def geqrf!
232
- # The real implementation is in lib/nmatrix/lapacke.rb
233
- raise(NotImplementedError, "geqrf! requires the nmatrix-lapacke gem")
234
- end
235
-
236
- #
237
- # call-seq:
238
- # ormqr(tau) -> NMatrix
239
- # ormqr(tau, side, transpose, c) -> NMatrix
240
- #
241
- # Returns the product Q * c or c * Q after a call to geqrf! used in QR factorization.
242
- # +c+ is overwritten with the elements of the result NMatrix if supplied. Q is the orthogonal matrix
243
- # represented by tau and the calling NMatrix
244
- #
245
- # Only works on float types, use unmqr for complex types.
246
- #
247
- # == Arguments
248
- #
249
- # * +tau+ - vector containing scalar factors of elementary reflectors
250
- # * +side+ - direction of multiplication [:left, :right]
251
- # * +transpose+ - apply Q with or without transpose [false, :transpose]
252
- # * +c+ - NMatrix multplication argument that is overwritten, no argument assumes c = identity
253
- #
254
- # * *Returns* :
255
- #
256
- # - Q * c or c * Q Where Q may be transposed before multiplication.
257
- #
258
- #
259
- # * *Raises* :
260
- # - +StorageTypeError+ -> LAPACK functions only work on dense matrices.
261
- # - +TypeError+ -> Works only on floating point matrices, use unmqr for complex types
262
- # - +TypeError+ -> c must have the same dtype as the calling NMatrix
263
- #
264
- def ormqr(tau, side=:left, transpose=false, c=nil)
265
- # The real implementation is in lib/nmatrix/lapacke.rb
266
- raise(NotImplementedError, "ormqr requires the nmatrix-lapacke gem")
267
-
268
- end
269
167
 
270
168
  #
271
169
  # call-seq:
272
- # unmqr(tau) -> NMatrix
273
- # unmqr(tau, side, transpose, c) -> NMatrix
170
+ # pinv -> NMatrix
274
171
  #
275
- # Returns the product Q * c or c * Q after a call to geqrf! used in QR factorization.
276
- # +c+ is overwritten with the elements of the result NMatrix if it is supplied. Q is the orthogonal matrix
277
- # represented by tau and the calling NMatrix
278
- #
279
- # Only works on complex types, use ormqr for float types.
172
+ # Compute the Moore-Penrose pseudo-inverse of a matrix using its
173
+ # singular value decomposition (SVD).
280
174
  #
281
- # == Arguments
175
+ # This function requires the nmatrix-atlas gem installed.
282
176
  #
283
- # * +tau+ - vector containing scalar factors of elementary reflectors
284
- # * +side+ - direction of multiplication [:left, :right]
285
- # * +transpose+ - apply Q as Q or its complex conjugate [false, :complex_conjugate]
286
- # * +c+ - NMatrix multplication argument that is overwritten, no argument assumes c = identity
177
+ # * *Arguments* :
178
+ # - +tolerance(optional)+ -> Cutoff for small singular values.
287
179
  #
288
180
  # * *Returns* :
289
- #
290
- # - Q * c or c * Q Where Q may be transformed to its complex conjugate before multiplication.
291
- #
181
+ # - Pseudo-inverse matrix.
292
182
  #
293
183
  # * *Raises* :
294
- # - +StorageTypeError+ -> LAPACK functions only work on dense matrices.
295
- # - +TypeError+ -> Works only on floating point matrices, use unmqr for complex types
296
- # - +TypeError+ -> c must have the same dtype as the calling NMatrix
184
+ # - +NotImplementedError+ -> If called without nmatrix-atlas or nmatrix-lapacke gem.
185
+ # - +TypeError+ -> If called without float or complex data type.
297
186
  #
298
- def unmqr(tau, side=:left, transpose=false, c=nil)
299
- # The real implementation is in lib/nmatrix/lapacke.rb
300
- raise(NotImplementedError, "unmqr requires the nmatrix-lapacke gem")
301
- end
302
-
187
+ # * *Examples* :
303
188
  #
304
- # call-seq:
305
- # potrf!(upper_or_lower) -> NMatrix
189
+ # a = NMatrix.new([2,2],[1,2,
190
+ # 3,4], dtype: :float64)
191
+ # a.pinv # => [ [-2.0000000000000018, 1.0000000000000007]
192
+ # [1.5000000000000016, -0.5000000000000008] ]
306
193
  #
307
- # Cholesky factorization of a symmetric positive-definite matrix -- or, if complex,
308
- # a Hermitian positive-definite matrix +A+.
309
- # The result will be written in either the upper or lower triangular portion of the
310
- # matrix, depending on whether the argument is +:upper+ or +:lower+.
311
- # Also the function only reads in the upper or lower part of the matrix,
312
- # so it doesn't actually have to be symmetric/Hermitian.
313
- # However, if the matrix (i.e. the symmetric matrix implied by the lower/upper
314
- # half) is not positive-definite, the function will return nonsense.
194
+ # b = NMatrix.new([4,1],[1,2,3,4], dtype: :float64)
195
+ # b.pinv # => [ [ 0.03333333, 0.06666667, 0.99999999, 0.13333333] ]
315
196
  #
316
- # This functions requires either the nmatrix-atlas or nmatrix-lapacke gem
317
- # installed.
197
+ # == References
318
198
  #
319
- # * *Returns* :
320
- # the triangular portion specified by the parameter
321
- # * *Raises* :
322
- # - +StorageTypeError+ -> ATLAS functions only work on dense matrices.
323
- # - +ShapeError+ -> Must be square.
324
- # - +NotImplementedError+ -> If called without nmatrix-atlas or nmatrix-lapacke gem
199
+ # * https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_pseudoinverse
200
+ # * G. Strang, Linear Algebra and Its Applications, 2nd Ed., Orlando, FL, Academic Press
325
201
  #
326
- def potrf!(which)
327
- # The real implementation is in the plugin files.
328
- raise(NotImplementedError, "potrf! requires either the nmatrix-atlas or nmatrix-lapacke gem")
329
- end
330
-
331
- def potrf_upper!
332
- potrf! :upper
333
- end
334
-
335
- def potrf_lower!
336
- potrf! :lower
202
+ def pinv(tolerance = 1e-15)
203
+ raise DataTypeError, "pinv works only with matrices of float or complex data type" unless
204
+ [:float32, :float64, :complex64, :complex128].include?(dtype)
205
+ if self.complex_dtype?
206
+ u, s, vt = self.complex_conjugate.gesvd # singular value decomposition
207
+ else
208
+ u, s, vt = self.gesvd
209
+ end
210
+ rows = self.shape[0]
211
+ cols = self.shape[1]
212
+ if rows < cols
213
+ u_reduced = u
214
+ vt_reduced = vt[0..rows - 1, 0..cols - 1].transpose
215
+ else
216
+ u_reduced = u[0..rows - 1, 0..cols - 1]
217
+ vt_reduced = vt.transpose
218
+ end
219
+ largest_singular_value = s.max.to_f
220
+ cutoff = tolerance * largest_singular_value
221
+ (0...[rows, cols].min).each do |i|
222
+ s[i] = 1 / s[i] if s[i] > cutoff
223
+ s[i] = 0 if s[i] <= cutoff
224
+ end
225
+ multiplier = u_reduced.dot(NMatrix.diagonal(s.to_a)).transpose
226
+ vt_reduced.dot(multiplier)
337
227
  end
228
+ alias :pseudo_inverse :pinv
229
+ alias :pseudoinverse :pinv
338
230
 
339
231
 
340
232
  #
341
233
  # call-seq:
342
- # factorize_cholesky -> [upper NMatrix, lower NMatrix]
343
- #
344
- # Calculates the Cholesky factorization of a matrix and returns the
345
- # upper and lower matrices such that A=LU and L=U*, where * is
346
- # either the transpose or conjugate transpose.
347
- #
348
- # Unlike potrf!, this makes method requires that the original is matrix is
349
- # symmetric or Hermitian. However, it is still your responsibility to make
350
- # sure it is positive-definite.
351
- def factorize_cholesky
352
- raise "Matrix must be symmetric/Hermitian for Cholesky factorization" unless self.hermitian?
353
- l = self.clone.potrf_lower!.tril!
354
- u = l.conjugate_transpose
355
- [u,l]
356
- end
357
-
234
+ # adjugate! -> NMatrix
358
235
  #
359
- # call-seq:
360
- # factorize_lu -> ...
236
+ # Calculate the adjugate of the matrix (in-place).
237
+ # Only works on dense matrices.
361
238
  #
362
- # LU factorization of a matrix. Optionally return the permutation matrix.
363
- # Note that computing the permutation matrix will introduce a slight memory
364
- # and time overhead.
365
- #
366
- # == Arguments
367
- #
368
- # +with_permutation_matrix+ - If set to *true* will return the permutation
369
- # matrix alongwith the LU factorization as a second return value.
370
- #
371
- def factorize_lu with_permutation_matrix=nil
372
- raise(NotImplementedError, "only implemented for dense storage") unless self.stype == :dense
373
- raise(NotImplementedError, "matrix is not 2-dimensional") unless self.dimensions == 2
374
-
375
- t = self.clone
376
- pivot = t.getrf!
377
- return t unless with_permutation_matrix
378
-
379
- [t, FactorizeLUMethods.permutation_matrix_from(pivot)]
239
+ # * *Raises* :
240
+ # - +StorageTypeError+ -> only implemented on dense matrices.
241
+ # - +ShapeError+ -> matrix must be square.
242
+ # - +DataTypeError+ -> cannot calculate adjugate of an integer matrix in-place.
243
+ #
244
+ def adjugate!
245
+ raise(StorageTypeError, "adjugate only works on dense matrices currently") unless self.dense?
246
+ raise(ShapeError, "Cannot calculate adjugate of a non-square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
247
+ raise(DataTypeError, "Cannot calculate adjugate of an integer matrix in-place") if self.integer_dtype?
248
+ d = self.det
249
+ self.invert!
250
+ self.map! { |e| e * d }
251
+ self
380
252
  end
253
+ alias :adjoint! :adjugate!
381
254
 
382
255
  #
383
256
  # call-seq:
384
- # factorize_qr -> [Q,R]
385
- #
386
- # QR factorization of a matrix without column pivoting.
387
- # Q is orthogonal and R is upper triangular if input is square or upper trapezoidal if
388
- # input is rectangular.
257
+ # adjugate -> NMatrix
389
258
  #
390
- # Only works for dense matrices.
259
+ # Make a copy of the matrix and calculate the adjugate of the matrix.
260
+ # Only works on dense matrices.
391
261
  #
392
262
  # * *Returns* :
393
- # - Array containing Q and R matrices
263
+ # - A dense NMatrix. Will be the same type as the input NMatrix,
264
+ # except if the input is an integral dtype, in which case it will be a
265
+ # :float64 NMatrix.
394
266
  #
395
267
  # * *Raises* :
396
- # - +StorageTypeError+ -> only implemented for desnse storage.
397
- # - +ShapeError+ -> Input must be a 2-dimensional matrix to have a QR decomposition.
398
- #
399
- def factorize_qr
400
- raise(NotImplementedError, "only implemented for dense storage") unless self.stype == :dense
401
- raise(ShapeError, "Input must be a 2-dimensional matrix to have a QR decomposition") unless self.dim == 2
402
-
403
- rows, columns = self.shape
404
- r = self.clone
405
- tau = r.geqrf!
406
-
407
- #Obtain Q
408
- q = self.complex_dtype? ? r.unmqr(tau) : r.ormqr(tau)
409
-
410
- #Obtain R
411
- if rows <= columns
412
- r.upper_triangle!
413
- #Need to account for upper trapezoidal structure if R is a tall rectangle (rows > columns)
414
- else
415
- r[0...columns, 0...columns].upper_triangle!
416
- r[columns...rows, 0...columns] = 0
417
- end
418
-
419
- [q,r]
268
+ # - +StorageTypeError+ -> only implemented on dense matrices.
269
+ # - +ShapeError+ -> matrix must be square.
270
+ #
271
+ def adjugate
272
+ raise(StorageTypeError, "adjugate only works on dense matrices currently") unless self.dense?
273
+ raise(ShapeError, "Cannot calculate adjugate of a non-square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
274
+ d = self.det
275
+ mat = self.invert
276
+ mat.map! { |e| e * d }
277
+ mat
420
278
  end
279
+ alias :adjoint :adjugate
421
280
 
422
281
  # Reduce self to upper hessenberg form using householder transforms.
423
- #
282
+ #
424
283
  # == References
425
284
  #
426
285
  # * http://en.wikipedia.org/wiki/Hessenberg_matrix
@@ -431,331 +290,76 @@ class NMatrix
431
290
 
432
291
  # Destructive version of #hessenberg
433
292
  def hessenberg!
434
- raise ShapeError, "Trying to reduce non 2D matrix to hessenberg form" if
293
+ raise ShapeError, "Trying to reduce non 2D matrix to hessenberg form" if
435
294
  shape.size != 2
436
- raise ShapeError, "Trying to reduce non-square matrix to hessenberg form" if
295
+ raise ShapeError, "Trying to reduce non-square matrix to hessenberg form" if
437
296
  shape[0] != shape[1]
438
297
  raise StorageTypeError, "Matrix must be dense" if stype != :dense
439
- raise TypeError, "Works with float matrices only" unless
298
+ raise TypeError, "Works with float matrices only" unless
440
299
  [:float64,:float32].include?(dtype)
441
300
 
442
301
  __hessenberg__(self)
443
302
  self
444
303
  end
445
304
 
446
- # Solve the matrix equation AX = B, where A is +self+, B is the first
447
- # argument, and X is returned. A must be a nxn square matrix, while B must be
448
- # nxm. Only works with dense matrices and non-integer, non-object data types.
449
- #
450
- # == Arguments
451
- #
452
- # * +b+ - the right hand side
453
- #
454
- # == Options
455
- #
456
- # * +form+ - Signifies the form of the matrix A in the linear system AX=B.
457
- # If not set then it defaults to +:general+, which uses an LU solver.
458
- # Other possible values are +:lower_tri+, +:upper_tri+ and +:pos_def+ (alternatively,
459
- # non-abbreviated symbols +:lower_triangular+, +:upper_triangular+,
460
- # and +:positive_definite+ can be used.
461
- # If +:lower_tri+ or +:upper_tri+ is set, then a specialized linear solver for linear
462
- # systems AX=B with a lower or upper triangular matrix A is used. If +:pos_def+ is chosen,
463
- # then the linear system is solved via the Cholesky factorization.
464
- # Note that when +:lower_tri+ or +:upper_tri+ is used, then the algorithm just assumes that
465
- # all entries in the lower/upper triangle of the matrix are zeros without checking (which
466
- # can be useful in certain applications).
467
- #
468
- #
469
- # == Usage
470
- #
471
- # a = NMatrix.new [2,2], [3,1,1,2], dtype: dtype
472
- # b = NMatrix.new [2,1], [9,8], dtype: dtype
473
- # a.solve(b)
474
- #
475
- # # solve an upper triangular linear system more efficiently:
476
- # require 'benchmark'
477
- # require 'nmatrix/lapacke'
478
- # rand_mat = NMatrix.random([10000, 10000], dtype: :float64)
479
- # a = rand_mat.triu
480
- # b = NMatrix.random([10000, 10], dtype: :float64)
481
- # Benchmark.bm(10) do |bm|
482
- # bm.report('general') { a.solve(b) }
483
- # bm.report('upper_tri') { a.solve(b, form: :upper_tri) }
484
- # end
485
- # # user system total real
486
- # # general 73.170000 0.670000 73.840000 ( 73.810086)
487
- # # upper_tri 0.180000 0.000000 0.180000 ( 0.182491)
488
- #
489
- def solve(b, opts = {})
490
- raise(ShapeError, "Must be called on square matrix") unless self.dim == 2 && self.shape[0] == self.shape[1]
491
- raise(ShapeError, "number of rows of b must equal number of cols of self") if
492
- self.shape[1] != b.shape[0]
493
- raise(ArgumentError, "only works with dense matrices") if self.stype != :dense
494
- raise(ArgumentError, "only works for non-integer, non-object dtypes") if
495
- integer_dtype? or object_dtype? or b.integer_dtype? or b.object_dtype?
496
-
497
- opts = { form: :general }.merge(opts)
498
- x = b.clone
499
- n = self.shape[0]
500
- nrhs = b.shape[1]
501
-
502
- case opts[:form]
503
- when :general
504
- clone = self.clone
505
- ipiv = NMatrix::LAPACK.clapack_getrf(:row, n, n, clone, n)
506
- # When we call clapack_getrs with :row, actually only the first matrix
507
- # (i.e. clone) is interpreted as row-major, while the other matrix (x)
508
- # is interpreted as column-major. See here: http://math-atlas.sourceforge.net/faq.html#RowSolve
509
- # So we must transpose x before and after
510
- # calling it.
511
- x = x.transpose
512
- NMatrix::LAPACK.clapack_getrs(:row, :no_transpose, n, nrhs, clone, n, ipiv, x, n)
513
- x.transpose
514
- when :upper_tri, :upper_triangular
515
- raise(ArgumentError, "upper triangular solver does not work with complex dtypes") if
516
- complex_dtype? or b.complex_dtype?
517
- # this is the correct function call; see https://github.com/SciRuby/nmatrix/issues/374
518
- NMatrix::BLAS::cblas_trsm(:row, :left, :upper, false, :nounit, n, nrhs, 1.0, self, n, x, nrhs)
519
- x
520
- when :lower_tri, :lower_triangular
521
- raise(ArgumentError, "lower triangular solver does not work with complex dtypes") if
522
- complex_dtype? or b.complex_dtype?
523
- NMatrix::BLAS::cblas_trsm(:row, :left, :lower, false, :nounit, n, nrhs, 1.0, self, n, x, nrhs)
524
- x
525
- when :pos_def, :positive_definite
526
- u, l = self.factorize_cholesky
527
- z = l.solve(b, form: :lower_tri)
528
- u.solve(z, form: :upper_tri)
529
- else
530
- raise(ArgumentError, "#{opts[:form]} is not a valid form option")
531
- end
532
- end
533
-
534
- #
535
- # call-seq:
536
- # gesvd! -> [u, sigma, v_transpose]
537
- # gesvd! -> [u, sigma, v_conjugate_transpose] # complex
538
- #
539
- # Compute the singular value decomposition of a matrix using LAPACK's GESVD function.
540
- # This is destructive, modifying the source NMatrix. See also #gesdd.
541
- #
542
- # Optionally accepts a +workspace_size+ parameter, which will be honored only if it is larger than what LAPACK
543
- # requires.
544
- #
545
- def gesvd!(workspace_size=1)
546
- NMatrix::LAPACK::gesvd(self, workspace_size)
547
- end
548
-
549
- #
550
- # call-seq:
551
- # gesvd -> [u, sigma, v_transpose]
552
- # gesvd -> [u, sigma, v_conjugate_transpose] # complex
553
- #
554
- # Compute the singular value decomposition of a matrix using LAPACK's GESVD function.
555
- #
556
- # Optionally accepts a +workspace_size+ parameter, which will be honored only if it is larger than what LAPACK
557
- # requires.
558
- #
559
- def gesvd(workspace_size=1)
560
- self.clone.gesvd!(workspace_size)
561
- end
562
-
563
-
564
-
565
- #
566
- # call-seq:
567
- # gesdd! -> [u, sigma, v_transpose]
568
- # gesdd! -> [u, sigma, v_conjugate_transpose] # complex
569
- #
570
- # Compute the singular value decomposition of a matrix using LAPACK's GESDD function. This uses a divide-and-conquer
571
- # strategy. This is destructive, modifying the source NMatrix. See also #gesvd.
572
- #
573
- # Optionally accepts a +workspace_size+ parameter, which will be honored only if it is larger than what LAPACK
574
- # requires.
575
- #
576
- def gesdd!(workspace_size=nil)
577
- NMatrix::LAPACK::gesdd(self, workspace_size)
578
- end
579
-
580
- #
581
- # call-seq:
582
- # gesdd -> [u, sigma, v_transpose]
583
- # gesdd -> [u, sigma, v_conjugate_transpose] # complex
584
- #
585
- # Compute the singular value decomposition of a matrix using LAPACK's GESDD function. This uses a divide-and-conquer
586
- # strategy. See also #gesvd.
587
- #
588
- # Optionally accepts a +workspace_size+ parameter, which will be honored only if it is larger than what LAPACK
589
- # requires.
590
- #
591
- def gesdd(workspace_size=nil)
592
- self.clone.gesdd!(workspace_size)
593
- end
594
-
595
- #
596
- # call-seq:
597
- # laswp!(ary) -> NMatrix
598
- #
599
- # In-place permute the columns of a dense matrix using LASWP according to the order given as an array +ary+.
600
- #
601
- # If +:convention+ is +:lapack+, then +ary+ represents a sequence of pair-wise permutations which are
602
- # performed successively. That is, the i'th entry of +ary+ is the index of the column to swap
603
- # the i'th column with, having already applied all earlier swaps.
604
- #
605
- # If +:convention+ is +:intuitive+, then +ary+ represents the order of columns after the permutation.
606
- # That is, the i'th entry of +ary+ is the index of the column that will be in position i after the
607
- # reordering (Matlab-like behaviour). This is the default.
608
- #
609
- # Not yet implemented for yale or list.
610
- #
611
- # == Arguments
612
- #
613
- # * +ary+ - An Array specifying the order of the columns. See above for details.
614
- #
615
- # == Options
616
- #
617
- # * +:covention+ - Possible values are +:lapack+ and +:intuitive+. Default is +:intuitive+. See above for details.
618
- #
619
- def laswp!(ary, opts={})
620
- raise(StorageTypeError, "ATLAS functions only work on dense matrices") unless self.dense?
621
- opts = { convention: :intuitive }.merge(opts)
622
-
623
- if opts[:convention] == :intuitive
624
- if ary.length != ary.uniq.length
625
- raise(ArgumentError, "No duplicated entries in the order array are allowed under convention :intuitive")
626
- end
627
- n = self.shape[1]
628
- p = []
629
- order = (0...n).to_a
630
- 0.upto(n-2) do |i|
631
- p[i] = order.index(ary[i])
632
- order[i], order[p[i]] = order[p[i]], order[i]
633
- end
634
- p[n-1] = n-1
635
- else
636
- p = ary
637
- end
638
-
639
- NMatrix::LAPACK::laswp(self, p)
640
- end
641
-
642
- #
643
- # call-seq:
644
- # laswp(ary) -> NMatrix
645
- #
646
- # Permute the columns of a dense matrix using LASWP according to the order given in an array +ary+.
647
- #
648
- # If +:convention+ is +:lapack+, then +ary+ represents a sequence of pair-wise permutations which are
649
- # performed successively. That is, the i'th entry of +ary+ is the index of the column to swap
650
- # the i'th column with, having already applied all earlier swaps. This is the default.
651
- #
652
- # If +:convention+ is +:intuitive+, then +ary+ represents the order of columns after the permutation.
653
- # That is, the i'th entry of +ary+ is the index of the column that will be in position i after the
654
- # reordering (Matlab-like behaviour).
655
- #
656
- # Not yet implemented for yale or list.
657
- #
658
- # == Arguments
659
- #
660
- # * +ary+ - An Array specifying the order of the columns. See above for details.
661
- #
662
- # == Options
663
- #
664
- # * +:covention+ - Possible values are +:lapack+ and +:intuitive+. Default is +:lapack+. See above for details.
665
- #
666
- def laswp(ary, opts={})
667
- self.clone.laswp!(ary, opts)
668
- end
669
305
 
670
- #
671
306
  # call-seq:
672
- # det -> determinant
673
- #
674
- # Calculate the determinant by way of LU decomposition. This is accomplished
675
- # using clapack_getrf, and then by taking the product of the diagonal elements. There is a
676
- # risk of underflow/overflow.
307
+ # matrix_norm -> Numeric
677
308
  #
678
- # There are probably also more efficient ways to calculate the determinant.
679
- # This method requires making a copy of the matrix, since clapack_getrf
680
- # modifies its input.
309
+ # Calculates the selected norm (defaults to 2-norm) of a 2D matrix.
681
310
  #
682
- # For smaller matrices, you may be able to use +#det_exact+.
311
+ # This should be used for small or medium sized matrices.
312
+ # For greater matrices, there should be a separate implementation where
313
+ # the norm is estimated rather than computed, for the sake of computation speed.
683
314
  #
684
- # This function is guaranteed to return the same type of data in the matrix
685
- # upon which it is called.
315
+ # Currently implemented norms are 1-norm, 2-norm, Frobenius, Infinity.
316
+ # A minus on the 1, 2 and inf norms returns the minimum instead of the maximum value.
686
317
  #
687
- # Integer matrices are converted to floating point matrices for the purposes of
688
- # performing the calculation, as xGETRF can't work on integer matrices.
318
+ # Tested mainly with dense matrices. Further checks and modifications might
319
+ # be necessary for sparse matrices.
689
320
  #
690
321
  # * *Returns* :
691
- # - The determinant of the matrix. It's the same type as the matrix's dtype.
322
+ # - The selected norm of the matrix.
692
323
  # * *Raises* :
693
- # - +ShapeError+ -> Must be used on square matrices.
694
- #
695
- def det
696
- raise(ShapeError, "determinant can be calculated only for square matrices") unless self.dim == 2 && self.shape[0] == self.shape[1]
697
-
698
- # Cast to a dtype for which getrf is implemented
699
- new_dtype = self.integer_dtype? ? :float64 : self.dtype
700
- copy = self.cast(:dense, new_dtype)
701
-
702
- # Need to know the number of permutations. We'll add up the diagonals of
703
- # the factorized matrix.
704
- pivot = copy.getrf!
705
-
706
- num_perm = 0 #number of permutations
707
- pivot.each_with_index do |swap, i|
708
- #pivot indexes rows starting from 1, instead of 0, so need to subtract 1 here
709
- num_perm += 1 if swap-1 != i
710
- end
711
- prod = num_perm % 2 == 1 ? -1 : 1 # odd permutations => negative
712
- [shape[0],shape[1]].min.times do |i|
713
- prod *= copy[i,i]
324
+ # - +NotImplementedError+ -> norm can be calculated only for 2D matrices
325
+ # - +ArgumentError+ -> unrecognized norm
326
+ #
327
+ def matrix_norm type = 2
328
+ raise(NotImplementedError, "norm can be calculated only for 2D matrices") unless self.dim == 2
329
+ raise(NotImplementedError, "norm only implemented for dense storage") unless self.stype == :dense
330
+ raise(ArgumentError, "norm not defined for byte dtype")if self.dtype == :byte
331
+ case type
332
+ when nil, 2, -2
333
+ return self.two_matrix_norm (type == -2)
334
+ when 1, -1
335
+ return self.one_matrix_norm (type == -1)
336
+ when :frobenius, :fro
337
+ return self.fro_matrix_norm
338
+ when :infinity, :inf, :'-inf', :'-infinity'
339
+ return self.inf_matrix_norm (type == :'-inf' || type == :'-infinity')
340
+ else
341
+ raise ArgumentError.new("argument must be a valid integer or symbol")
714
342
  end
715
-
716
- # Convert back to an integer if necessary
717
- new_dtype != self.dtype ? prod.round : prod #prevent rounding errors
718
- end
719
-
720
- #
721
- # call-seq:
722
- # complex_conjugate -> NMatrix
723
- # complex_conjugate(new_stype) -> NMatrix
724
- #
725
- # Get the complex conjugate of this matrix. See also complex_conjugate! for
726
- # an in-place operation (provided the dtype is already +:complex64+ or
727
- # +:complex128+).
728
- #
729
- # Doesn't work on list matrices, but you can optionally pass in the stype you
730
- # want to cast to if you're dealing with a list matrix.
731
- #
732
- # * *Arguments* :
733
- # - +new_stype+ -> stype for the new matrix.
734
- # * *Returns* :
735
- # - If the original NMatrix isn't complex, the result is a +:complex128+ NMatrix. Otherwise, it's the original dtype.
736
- #
737
- def complex_conjugate(new_stype = self.stype)
738
- self.cast(new_stype, NMatrix::upcast(dtype, :complex64)).complex_conjugate!
739
343
  end
740
344
 
741
345
  # Calculate the variance co-variance matrix
742
- #
346
+ #
743
347
  # == Options
744
- #
348
+ #
745
349
  # * +:for_sample_data+ - Default true. If set to false will consider the denominator for
746
350
  # population data (i.e. N, as opposed to N-1 for sample data).
747
- #
351
+ #
748
352
  # == References
749
- #
353
+ #
750
354
  # * http://stattrek.com/matrix-algebra/covariance-matrix.aspx
751
355
  def cov(opts={})
752
356
  raise TypeError, "Only works for non-integer dtypes" if integer_dtype?
753
357
  opts = {
754
358
  for_sample_data: true
755
359
  }.merge(opts)
756
-
360
+
757
361
  denominator = opts[:for_sample_data] ? rows - 1 : rows
758
- ones = NMatrix.ones [rows,1]
362
+ ones = NMatrix.ones [rows,1]
759
363
  deviation_scores = self - ones.dot(ones.transpose).dot(self) / rows
760
364
  deviation_scores.transpose.dot(deviation_scores) / denominator
761
365
  end
@@ -769,24 +373,24 @@ class NMatrix
769
373
 
770
374
  # Raise a square matrix to a power. Be careful of numeric overflows!
771
375
  # In case *n* is 0, an identity matrix of the same dimension is returned. In case
772
- # of negative *n*, the matrix is inverted and the absolute value of *n* taken
376
+ # of negative *n*, the matrix is inverted and the absolute value of *n* taken
773
377
  # for computing the power.
774
- #
378
+ #
775
379
  # == Arguments
776
- #
380
+ #
777
381
  # * +n+ - Integer to which self is to be raised.
778
- #
382
+ #
779
383
  # == References
780
- #
781
- # * R.G Dromey - How to Solve it by Computer. Link -
384
+ #
385
+ # * R.G Dromey - How to Solve it by Computer. Link -
782
386
  # 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
783
387
  def pow n
784
- raise ShapeError, "Only works with 2D square matrices." if
388
+ raise ShapeError, "Only works with 2D square matrices." if
785
389
  shape[0] != shape[1] or shape.size != 2
786
390
  raise TypeError, "Only works with integer powers" unless n.is_a?(Integer)
787
391
 
788
392
  sequence = (integer_dtype? ? self.cast(dtype: :int64) : self).clone
789
- product = NMatrix.eye shape[0], dtype: sequence.dtype, stype: sequence.stype
393
+ product = NMatrix.eye shape[0], dtype: sequence.dtype, stype: sequence.stype
790
394
 
791
395
  if n == 0
792
396
  return NMatrix.eye(shape, dtype: dtype, stype: stype)
@@ -814,8 +418,8 @@ class NMatrix
814
418
  #
815
419
  # * +mat+ - A 2D NMatrix object
816
420
  #
817
- # === Usage
818
- #
421
+ # === Usage
422
+ #
819
423
  # a = NMatrix.new([2,2],[1,2,
820
424
  # 3,4])
821
425
  # b = NMatrix.new([2,3],[1,1,1,
@@ -824,7 +428,7 @@ class NMatrix
824
428
  # [1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
825
429
  # [3.0, 3.0, 3.0, 4.0, 4.0, 4.0]
826
430
  # [3.0, 3.0, 3.0, 4.0, 4.0, 4.0] ]
827
- #
431
+ #
828
432
  def kron_prod(mat)
829
433
  unless self.dimensions==2 and mat.dimensions==2
830
434
  raise ShapeError, "Implemented for 2D NMatrix objects only."
@@ -849,21 +453,7 @@ class NMatrix
849
453
  end
850
454
  end
851
455
 
852
- NMatrix.new([n,m], kron_prod_array)
853
- end
854
-
855
- #
856
- # call-seq:
857
- # conjugate_transpose -> NMatrix
858
- #
859
- # Calculate the conjugate transpose of a matrix. If your dtype is already
860
- # complex, this should only require one copy (for the transpose).
861
- #
862
- # * *Returns* :
863
- # - The conjugate transpose of the matrix as a copy.
864
- #
865
- def conjugate_transpose
866
- self.transpose.complex_conjugate!
456
+ NMatrix.new([n,m], kron_prod_array)
867
457
  end
868
458
 
869
459
  #
@@ -910,7 +500,9 @@ class NMatrix
910
500
  ##
911
501
  # call-seq:
912
502
  # sum() -> NMatrix
503
+ # cumsum() -> NMatrix
913
504
  # sum(dimen) -> NMatrix
505
+ # cumsum(dimen) -> NMatrix
914
506
  #
915
507
  # Calculates the sum along the specified dimension.
916
508
  #
@@ -920,7 +512,7 @@ class NMatrix
920
512
  sum + sub_mat
921
513
  end
922
514
  end
923
-
515
+ alias :cumsum :sum
924
516
 
925
517
  ##
926
518
  # call-seq:
@@ -1033,79 +625,79 @@ class NMatrix
1033
625
  end.cast(self.stype, abs_dtype)
1034
626
  end
1035
627
 
1036
- #
1037
- # call-seq:
1038
- # absolute_sum -> Numeric
1039
- #
1040
- # == Arguments
1041
- # - +incx+ -> the skip size (defaults to 1, no skip)
1042
- # - +n+ -> the number of elements to include
1043
- #
1044
- # Return the sum of the contents of the vector. This is the BLAS asum routine.
1045
- def asum incx=1, n=nil
1046
- if self.shape == [1]
1047
- return self[0].abs unless self.complex_dtype?
1048
- return self[0].real.abs + self[0].imag.abs
1049
- end
1050
- return method_missing(:asum, incx, n) unless vector?
1051
- NMatrix::BLAS::asum(self, incx, self.size / incx)
628
+ # Norm calculation methods
629
+ # Frobenius norm: the Euclidean norm of the matrix, treated as if it were a vector
630
+ def fro_matrix_norm
631
+ #float64 has to be used in any case, since nrm2 will not yield correct result for float32
632
+ self_cast = self.cast(:dtype => :float64)
633
+
634
+ column_vector = self_cast.reshape([self.size, 1])
635
+
636
+ return column_vector.nrm2
1052
637
  end
1053
- alias :absolute_sum :asum
1054
638
 
1055
- #
1056
- # call-seq:
1057
- # norm2 -> Numeric
1058
- #
1059
- # == Arguments
1060
- # - +incx+ -> the skip size (defaults to 1, no skip)
1061
- # - +n+ -> the number of elements to include
1062
- #
1063
- # Return the 2-norm of the vector. This is the BLAS nrm2 routine.
1064
- def nrm2 incx=1, n=nil
1065
- return method_missing(:nrm2, incx, n) unless vector?
1066
- NMatrix::BLAS::nrm2(self, incx, self.size / incx)
639
+ # 2-norm: the largest/smallest singular value of the matrix
640
+ def two_matrix_norm minus = false
641
+
642
+ self_cast = self.cast(:dtype => :float64)
643
+
644
+ #TODO: confirm if this is the desired svd calculation
645
+ svd = self_cast.gesvd
646
+ return svd[1][0, 0] unless minus
647
+ return svd[1][svd[1].rows-1, svd[1].cols-1]
1067
648
  end
1068
- alias :norm2 :nrm2
1069
649
 
1070
- #
1071
- # call-seq:
1072
- # scale! -> NMatrix
1073
- #
1074
- # == Arguments
1075
- # - +alpha+ -> Scalar value used in the operation.
1076
- # - +inc+ -> Increment used in the scaling function. Should generally be 1.
1077
- # - +n+ -> Number of elements of +vector+.
1078
- #
1079
- # This is a destructive method, modifying the source NMatrix. See also #scale.
1080
- # Return the scaling result of the matrix. BLAS scal will be invoked if provided.
1081
-
1082
- def scale!(alpha, incx=1, n=nil)
1083
- raise(DataTypeError, "Incompatible data type for the scaling factor") unless
1084
- NMatrix::upcast(self.dtype, NMatrix::min_dtype(alpha)) == self.dtype
1085
- return NMatrix::BLAS::scal(alpha, self, incx, self.size / incx) if NMatrix::BLAS.method_defined? :scal
1086
- self.each_stored_with_indices do |e, *i|
1087
- self[*i] = e*alpha
650
+ # 1-norm: the maximum/minimum absolute column sum of the matrix
651
+ def one_matrix_norm minus = false
652
+ #TODO: change traversing method for sparse matrices
653
+ number_of_columns = self.cols
654
+ col_sums = []
655
+
656
+ number_of_columns.times do |i|
657
+ col_sums << self.col(i).inject(0) { |sum, number| sum += number.abs}
1088
658
  end
659
+
660
+ return col_sums.max unless minus
661
+ return col_sums.min
662
+ end
663
+
664
+ # Infinity norm: the maximum/minimum absolute row sum of the matrix
665
+ def inf_matrix_norm minus = false
666
+ number_of_rows = self.rows
667
+ row_sums = []
668
+
669
+ number_of_rows.times do |i|
670
+ row_sums << self.row(i).inject(0) { |sum, number| sum += number.abs}
671
+ end
672
+
673
+ return row_sums.max unless minus
674
+ return row_sums.min
1089
675
  end
1090
676
 
1091
677
  #
1092
678
  # call-seq:
1093
- # scale -> NMatrix
679
+ # positive_definite? -> boolean
1094
680
  #
1095
- # == Arguments
1096
- # - +alpha+ -> Scalar value used in the operation.
1097
- # - +inc+ -> Increment used in the scaling function. Should generally be 1.
1098
- # - +n+ -> Number of elements of +vector+.
1099
- #
1100
- # Return the scaling result of the matrix. BLAS scal will be invoked if provided.
1101
-
1102
- def scale(alpha, incx=1, n=nil)
1103
- return self.clone.scale!(alpha, incx, n)
681
+ # A matrix is positive definite if it’s symmetric and all its eigenvalues are positive
682
+ #
683
+ # * *Returns* :
684
+ # - A boolean value telling if the NMatrix is positive definite or not.
685
+ # * *Raises* :
686
+ # - +ShapeError+ -> Must be used on square matrices.
687
+ #
688
+ def positive_definite?
689
+ raise(ShapeError, "positive definite calculated only for square matrices") unless
690
+ self.dim == 2 && self.shape[0] == self.shape[1]
691
+ cond = 0
692
+ while cond != self.cols
693
+ if self[0..cond, 0..cond].det <= 0
694
+ return false
695
+ end
696
+ cond += 1
697
+ end
698
+ true
1104
699
  end
1105
700
 
1106
- alias :permute_columns :laswp
1107
- alias :permute_columns! :laswp!
1108
-
1109
701
  protected
1110
702
  # Define the element-wise operations for lists. Note that the __list_map_merged_stored__ iterator returns a Ruby Object
1111
703
  # matrix, which we then cast back to the appropriate type. If you don't want that, you can redefine these functions in
@@ -1321,3 +913,9 @@ protected
1321
913
  end
1322
914
  end
1323
915
  end
916
+
917
+ if jruby?
918
+ require_relative "./jruby/math.rb"
919
+ else
920
+ require_relative "./cruby/math.rb"
921
+ end