nulin 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/doc/BSDL +22 -0
- data/doc/COPYING +10 -0
- data/doc/README.en +31 -0
- data/doc/README.ja +36 -0
- data/ext/extconf.rb +33 -0
- data/ext/nulin_native.c +637 -0
- data/lib/narray_extext.rb +113 -0
- data/lib/nulin.rb +51 -0
- data/lib/nulin/cholesky.rb +62 -0
- data/lib/nulin/det.rb +17 -0
- data/lib/nulin/eigensystem.rb +191 -0
- data/lib/nulin/gemm.rb +53 -0
- data/lib/nulin/lls.rb +133 -0
- data/lib/nulin/qr.rb +68 -0
- data/lib/nulin/svd.rb +84 -0
- data/tests/run_test.rb +17 -0
- data/tests/test_cholesky.rb +39 -0
- data/tests/test_det.rb +11 -0
- data/tests/test_eigensystem.rb +71 -0
- data/tests/test_gemm.rb +57 -0
- data/tests/test_lls.rb +51 -0
- data/tests/test_narray.rb +100 -0
- data/tests/test_qr.rb +56 -0
- data/tests/test_svd.rb +51 -0
- metadata +108 -0
@@ -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
|
data/lib/nulin.rb
ADDED
@@ -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
|
data/lib/nulin/det.rb
ADDED
@@ -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
|
data/lib/nulin/gemm.rb
ADDED
@@ -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
|