nulin 0.2

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.
@@ -0,0 +1,113 @@
1
+ require 'narray'
2
+
3
+ class NArray
4
+ def square?
5
+ shape = self.shape
6
+ rank == 2 && shape[0] == shape[1]
7
+ end
8
+
9
+ def real?
10
+ typecode == DFLOAT || typecode == SFLOAT
11
+ end
12
+
13
+ def self.build(typecode, *sizes)
14
+ nary = new(typecode, *sizes)
15
+ (0 ... nary.size).each{|i| nary[i] = yield(*nary.ndindex_from_1dindex(i)) }
16
+ nary
17
+ end
18
+
19
+ def ndindex_from_1dindex(i)
20
+ ret = []
21
+ shape.each{|n| ret << i%n; i /= n }
22
+ ret
23
+ end
24
+
25
+ def each_with_index
26
+ (0 ... size).each{|i| yield(self[i], *ndindex_from_1dindex(i)) }
27
+ end
28
+
29
+ def self.complex_typecode?(typecode)
30
+ typecode == NArray::DCOMPLEX || typecode == NArray::SCOMPLEX
31
+ end
32
+
33
+ def self.filled(typecode, data, *sizes)
34
+ naray = new(typecode, *sizes)
35
+ naray.fill(data)
36
+ naray
37
+ end
38
+ end
39
+
40
+ class NMatrix
41
+ def row_vector(i)
42
+ NVector.ref(NArray.ref(self)[true, i, false])
43
+ end
44
+
45
+ def row_vectors
46
+ d = self.shape[1]
47
+ (0 ... d).map{|i| row_vector(i) }
48
+ end
49
+
50
+ def column_vector(i)
51
+ NVector.ref(NArray.ref(self)[i, false])
52
+ end
53
+
54
+ def column_vectors
55
+ d = self.shape[0]
56
+ (0 ... d).map{|i| column_vector(i) }
57
+ end
58
+
59
+ def adjoint
60
+ ret = self.transpose; ret.conj!
61
+ ret
62
+ end
63
+
64
+ def as_vector
65
+ if rank != 2 || (shape[0] != 1 && shape[1] != 1)
66
+ raise(ArgumentError,
67
+ "NMatrix#as_vector: the matrix should be column matrix" +
68
+ "or row matrix")
69
+ end
70
+
71
+ NVector.ref(flatten)
72
+ end
73
+
74
+ def self.diagonal(a, shape=[a.size, a.size], typecode=nil)
75
+ typecode ||= array2typecode(a)
76
+ m = NMatrix.new(typecode, *shape)
77
+ a = a.refer if a.kind_of?(NArray)
78
+ m.diagonal!(a)
79
+
80
+ m
81
+ end
82
+
83
+ def self.array2typecode(ary)
84
+ if ary.kind_of?(NArray)
85
+ ary.typecode
86
+ else
87
+ case ary[0]
88
+ when Float then NArray::DFLOAT
89
+ when Integer then NArray::INT
90
+ when Complex then NArray::DCOMPLEX
91
+ else NArray::OBJECT
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.I(n, typecode=NArray::DFLOAT)
97
+ m = NMatrix.new(typecode, n, n)
98
+ m.I
99
+
100
+ m
101
+ end
102
+
103
+ end
104
+
105
+ class NVector
106
+ def normalize
107
+ self / norm
108
+ end
109
+
110
+ def norm
111
+ Math.sqrt(self.mul_add(self.conjugate, 0).real)
112
+ end
113
+ end
@@ -0,0 +1,51 @@
1
+ require 'narray'
2
+ require 'narray_extext'
3
+ require 'singleton'
4
+ require 'nulin_native'
5
+
6
+ module NuLin
7
+ class DimensionError < StandardError
8
+ end
9
+
10
+ class LinalgError < StandardError
11
+ end
12
+
13
+ TYPECODES = [NArray::SFLOAT, NArray::DFLOAT, NArray::SCOMPLEX, NArray::DCOMPLEX]
14
+
15
+ LAPACK_PREFIX = {
16
+ NArray::SFLOAT => "s", NArray::DFLOAT => "d",
17
+ NArray::SCOMPLEX => "c", NArray::DCOMPLEX => "z",
18
+ }
19
+
20
+ module_function
21
+ def to_real_typecode(typecode)
22
+ return NArray::DFLOAT if typecode == NArray::DCOMPLEX
23
+ return NArray::SFLOAT if typecode == NArray::SCOMPLEX
24
+ return typecode
25
+ end
26
+
27
+ def to_complex_typecode(typecode)
28
+ return NArray::SCOMPLEX if typecode == NArray::SFLOAT
29
+ return NArray::DCOMPLEX
30
+ end
31
+
32
+ module Native
33
+ module_function
34
+ def call(typecode, name, *args)
35
+ fun_name = LAPACK_PREFIX[typecode] + name
36
+ retvals = __send__(fun_name, *args)
37
+ info = retvals ? retvals.last : 0
38
+ raise LinalgError, "#{fun_name}: errno #{info}" if info != 0
39
+
40
+ return retvals
41
+ end
42
+ end
43
+ end
44
+
45
+ require 'nulin/eigensystem'
46
+ require 'nulin/svd'
47
+ require 'nulin/lls'
48
+ require 'nulin/qr'
49
+ require 'nulin/det'
50
+ require 'nulin/cholesky'
51
+ require 'nulin/gemm'
@@ -0,0 +1,62 @@
1
+ module NuLin
2
+ module_function
3
+ # Compute the Cholesky decomposition for a positive definite
4
+ # symmetric/Hermitian `matrix`.
5
+ #
6
+ # Options
7
+ # * :type (default :U) - select whether the result matrix
8
+ # is upper(:U) or lower(:L).
9
+ #
10
+ # If the matrix is not positive definite, NuLin::LinalgError is raised.
11
+ #
12
+ # @note This method doesn't validate the matrix is symmetric/Hermitian.
13
+ # The only upper/lower half of the matrix is used for the computation.
14
+ # @param matrix[NMatrix] target matrix
15
+ # @param options[Hash] option hash
16
+ def cholesky(matrix, options={})
17
+ unless matrix.square?
18
+ raise DimensionError, "Cholesky decomposition is computable for a square matrix"
19
+ end
20
+
21
+ return Cholesky.new(matrix, options).result
22
+ end
23
+ end
24
+
25
+ class NuLin::Cholesky
26
+ def initialize(matrix, options)
27
+ @a = matrix
28
+ @typecode = matrix.typecode
29
+ case options.fetch(:type, :U)
30
+ when :U then @upper_triangular = true
31
+ when :L then @upper_triangular = false
32
+ else raise ArgumentError, "NuLin.cholesky: :type argument should be :U or :L"
33
+ end
34
+ compute
35
+ end
36
+
37
+ attr_reader :result
38
+
39
+ def compute
40
+ n, = @a.shape
41
+ @result = @a.transpose
42
+ NuLin::Native.call(@typecode, "potrf", @upper_triangular ? "L" : "U",
43
+ n, @result, n, 0)
44
+ if @upper_triangular
45
+ clear_lower
46
+ else
47
+ clear_upper
48
+ end
49
+
50
+ @result.conj!
51
+ end
52
+
53
+ def clear_lower
54
+ n, = @result.shape
55
+ 0.upto(n-1){|i| (i+1).upto(n-1){|j| @result[i, j] = 0.0 } }
56
+ end
57
+
58
+ def clear_upper
59
+ n, = @result.shape
60
+ 0.upto(n-1){|i| (i+1).upto(n-1){|j| @result[j, i] = 0.0 } }
61
+ end
62
+ end
@@ -0,0 +1,17 @@
1
+ module NuLin
2
+ module_function
3
+ # Compute the determinant of the `matrix` and return it.
4
+ #
5
+ # You get an exception NuLin::DimensionError if the matrix is not square
6
+ # The determinant of given matrix is computed using QR factorization.
7
+ #
8
+ # @param matrix[NMatrix] target matrix
9
+ def det(matrix)
10
+ unless matrix.square?
11
+ raise DimensionError, "Determinant is computable only on a square matrix"
12
+ end
13
+ n, = matrix.shape
14
+ r = qr(matrix).R
15
+ (0 ... n).inject(1.0){|u, i| u*r[i,i] }
16
+ end
17
+ end
@@ -0,0 +1,191 @@
1
+
2
+ module NuLin
3
+ module_function
4
+
5
+ # Return the eigensystem(a pair of eigenvalues and left/right eigenvectors)
6
+ # of `matrix` as an instance of (a subclass of) EigenDecomposition
7
+ #
8
+ # You can call this with some `options` as follows
9
+ # * :use_complex (default true) Return complex NArray objects
10
+ # If you enable this, the return object (EigenDecomposition)
11
+ # holds complex NArray objects.
12
+ # If you disable this, it is assumed that all eigenvalues are
13
+ # real and the return object holds the real NArray objects.
14
+ # If you disable this but there is a complex eigenvalue,
15
+ # the exception ArgumentError is raised.
16
+ # This options is ignored if `matrix` is complex.
17
+ # * :use_right (default true) - Compute right eigenvectors
18
+ # * :use_left (default true) - Compute left eigenvectors
19
+ # @param matrix [NMatrix] target matrix
20
+ # @param options [Hash] options
21
+ def eigensystem(matrix, options={})
22
+ unless matrix.square?
23
+ raise DimensionError, "The eigensystem is computable only for a square matrix"
24
+ end
25
+
26
+ case
27
+ when matrix.real?
28
+ RealEigenDecomposition.new(matrix, options)
29
+ when matrix.complex?
30
+ ComplexEigenDecomposition.new(matrix, options)
31
+ else
32
+ raise ArgumentError, "The eigensystem is comptable for a complex/float matrix"
33
+ end
34
+ end
35
+
36
+ class EigenDecomposition
37
+ # Dimension of the linear space
38
+ attr_reader :dim
39
+ # All eigenvalues as NArray object
40
+ attr_reader :eigenvalues
41
+ # Left eigenvectors as NMatrix
42
+ attr_reader :left
43
+ # Right eigenvectors as NMatrix
44
+ attr_reader :right
45
+
46
+ # Returns all right eigenvectors as the array of NVector
47
+ def right_eigenvectors
48
+ @right_eigenvectors ||= @right.column_vectors
49
+ end
50
+
51
+ # Returns all right eigenvectors as the array of NVector
52
+ def left_eigenvectors
53
+ @left_eigenvectors ||= @left.row_vectors
54
+ end
55
+ end
56
+
57
+ class RealEigenDecomposition < EigenDecomposition
58
+ def initialize(matrix, opts)
59
+ @matrix = matrix
60
+ @dim = matrix.shape[0]
61
+ @use_left = opts.fetch(:use_left, true)
62
+ @use_right = opts.fetch(:use_right, true)
63
+ @use_complex = opts.fetch(:use_complex, true)
64
+ @typecode = matrix.typecode
65
+ @complex_typecode = NuLin.to_complex_typecode(@typecode)
66
+
67
+ compute
68
+ end
69
+
70
+ private
71
+ def compute
72
+ n, = @matrix.shape
73
+ @wr = NArray.new(@typecode, n)
74
+ @wi = NArray.new(@typecode, n)
75
+ ldvl = @use_right ? n : 1
76
+ @vl = NArray.new(@typecode, ldvl, n)
77
+ ldvr = @use_left ? n : 1
78
+ @vr = NArray.new(@typecode, ldvr, n)
79
+ lwork = (@use_left || @use_right) ? 4*n : 3*n
80
+ work = NArray.new(@typecode, lwork)
81
+
82
+ NuLin::Native.call(@typecode, "geev",
83
+ @use_right ? 'V' : 'N', @use_left ? 'V' : 'N',
84
+ n, @matrix.dup, n, @wr, @wi, @vl, ldvl, @vr, ldvr,
85
+ work, lwork, 0)
86
+
87
+ if !@use_complex && !@wi.eq(0.0).all?
88
+ raise ArgumentError, "There is any complex eigenvalue"
89
+ end
90
+ compute_eigenvalues
91
+ compute_right if @use_right
92
+ compute_left if @use_left
93
+ end
94
+
95
+ def compute_eigenvalues
96
+ @eigenvalues = @use_complex ? complex_eigenvalues : real_eigenvalues
97
+ end
98
+
99
+ def real_eigenvalues
100
+ @wr
101
+ end
102
+
103
+ def complex_eigenvalues
104
+ eigenvalues = NArray.new(@complex_typecode, @dim)
105
+ eigenvalues[] = @wr
106
+ eigenvalues.imag = @wi
107
+ eigenvalues
108
+ end
109
+
110
+ # compute right eigenvectors and right eigenvector matrix
111
+ def compute_right
112
+ unless @use_complex
113
+ @right = NMatrix.ref(@vl).transpose
114
+ return
115
+ end
116
+
117
+ @right_eigenvectors = extract_complex_eigenvectors(@vl, -1)
118
+ @right = NMatrix.new(@complex_typecode, @dim, @dim)
119
+ @right_eigenvectors.map.with_index do |v, i|
120
+ @right[i, true] = v
121
+ end
122
+ end
123
+
124
+ # Extract eigenvectors from the result(vl or vr) of NumRu::Lapack.dgeev
125
+ def extract_complex_eigenvectors(m, sign)
126
+ eigenvectors = []
127
+ (0 ... @dim).each do |i|
128
+ if @wi[i] == 0.0
129
+ eigenvectors << NVector.ref(m[true, i])
130
+ elsif @wi[i] > 0.0
131
+ v = NVector.new(@complex_typecode, @dim)
132
+ v[] = m[true, i]; v.imag = m[true, i+1]
133
+ v.conj! if sign < 0
134
+ eigenvectors << v
135
+ else
136
+ eigenvectors << eigenvectors.last.conj
137
+ end
138
+ end
139
+
140
+ eigenvectors
141
+ end
142
+
143
+ # compute left eigenvectors and left eigenvector matrix
144
+ def compute_left
145
+ unless @use_complex
146
+ @left = NMatrix.ref(@vr)
147
+ return
148
+ end
149
+
150
+ @left_eigenvectors = extract_complex_eigenvectors(@vr, 1)
151
+ @left = NMatrix.new(@complex_typecode, @dim, @dim)
152
+ @left_eigenvectors.map.with_index do |v, i|
153
+ @left[true, i] = v
154
+ end
155
+ end
156
+ end
157
+
158
+ class ComplexEigenDecomposition < EigenDecomposition
159
+ def initialize(matrix, opts)
160
+ @matrix = matrix
161
+ @dim = matrix.shape[0]
162
+ @use_left = opts.fetch(:use_left, true)
163
+ @use_right = opts.fetch(:use_right, true)
164
+ @typecode = matrix.typecode
165
+
166
+ compute
167
+ end
168
+
169
+ private
170
+ # TODO: @left should be adjoint or not?
171
+ def compute
172
+ n, = @matrix.shape
173
+ @eigenvalues = NArray.new(@typecode, n)
174
+ ldvl = @use_right ? n : 1
175
+ vl = NMatrix.new(@typecode, ldvl, n)
176
+ ldvr = @use_left ? n : 1
177
+ vr = NMatrix.new(@typecode, ldvr, n)
178
+ lwork = 2*n
179
+ work = NArray.new(@typecode, lwork)
180
+ rwork = NArray.new(@typecode, 2*n)
181
+
182
+ NuLin::Native.call(@typecode, "geev",
183
+ @use_right ? 'V' : 'N', @use_left ? 'V' : 'N',
184
+ n, @matrix.dup, n, @eigenvalues,
185
+ vl, ldvl, vr, ldvr, work, lwork, rwork, 0)
186
+
187
+ @right = vl.adjoint
188
+ @left = vr
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,53 @@
1
+ module NuLin
2
+ module_function
3
+
4
+ # Return alpha*a*b + beta*c
5
+ def multiply_matrix_and_add(alpha, a, b, beta, c, options={})
6
+ c = c.dup if options.fetch(:overwrite_c, false)
7
+ trans_a = options.fetch(:trans_a, nil)
8
+ trans_b = options.fetch(:trans_b, nil)
9
+
10
+ if !trans_a
11
+ k, n = a.shape
12
+ else
13
+ n, k = a.shape
14
+ end
15
+ lda = a.shape[0]
16
+
17
+ if !trans_b
18
+ m, kk = b.shape
19
+ else
20
+ kk, m = b.shape
21
+ end
22
+ ldb = b.shape[0]
23
+
24
+ mm, nn = c.shape
25
+ ldc = mm
26
+
27
+ unless k == kk && m == mm && n == nn
28
+ raise DimensionError, "Invalid matrix shape"
29
+ end
30
+
31
+ NuLin::Native.call(a.typecode, "gemm",
32
+ mm_transpose_character(trans_b), mm_transpose_character(trans_a),
33
+ m, n, k, alpha, b, ldb, a, lda, beta, c, ldc)
34
+
35
+ c
36
+ end
37
+
38
+ def mm_transpose_character(t)
39
+ case t
40
+ when nil, false
41
+ "N"
42
+ when :T, :transpose
43
+ "T"
44
+ when :C, :H, :adjoint
45
+ "C"
46
+ else
47
+ raise ArgumentError, "Unkdnown transposing spec: #{t.inspect}"
48
+ end
49
+ end
50
+
51
+ alias mm multiply_matrix_and_add
52
+ module_function :mm
53
+ end