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.
- 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
|