Skip to content

Commit 40cc770

Browse files
npriyadarshitranslunar
authored andcommitted
Norms (cond.) (#586)
* made changes as per the feedback * added pending for jruby implementation
1 parent 30a7ddc commit 40cc770

File tree

2 files changed

+171
-6
lines changed

2 files changed

+171
-6
lines changed

lib/nmatrix/math.rb

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,48 @@ def laswp(ary, opts={})
803803
self.clone.laswp!(ary, opts)
804804
end
805805

806+
#
807+
# call-seq:
808+
# matrix_norm -> Numeric
809+
#
810+
# Calculates the selected norm (defaults to 2-norm) of a 2D matrix.
811+
#
812+
# This should be used for small or medium sized matrices.
813+
# For greater matrices, there should be a separate implementation where
814+
# the norm is estimated rather than computed, for the sake of computation speed.
815+
#
816+
# Currently implemented norms are 1-norm, 2-norm, Frobenius, Infinity.
817+
# A minus on the 1, 2 and inf norms returns the minimum instead of the maximum value.
818+
#
819+
# Tested mainly with dense matrices. Further checks and modifications might
820+
# be necessary for sparse matrices.
821+
#
822+
# * *Returns* :
823+
# - The selected norm of the matrix.
824+
# * *Raises* :
825+
# - +NotImplementedError+ -> norm can be calculated only for 2D matrices
826+
# - +ArgumentError+ -> unrecognized norm
827+
#
828+
def matrix_norm type = 2
829+
raise(NotImplementedError, "norm can be calculated only for 2D matrices") unless self.dim == 2
830+
raise(NotImplementedError, "norm only implemented for dense storage") unless self.stype == :dense
831+
raise(ArgumentError, "norm not defined for byte dtype")if self.dtype == :byte
832+
case type
833+
when nil, 2, -2
834+
return self.two_matrix_norm (type == -2)
835+
when 1, -1
836+
return self.one_matrix_norm (type == -1)
837+
when :frobenius, :fro
838+
return self.fro_matrix_norm
839+
when :infinity, :inf, :'-inf', :'-infinity'
840+
return self.inf_matrix_norm (type == :'-inf' || type == :'-infinity')
841+
else
842+
raise ArgumentError.new("argument must be a valid integer or symbol")
843+
end
844+
end
845+
846+
847+
806848
#
807849
# call-seq:
808850
# det -> determinant
@@ -1205,6 +1247,55 @@ def nrm2 incx=1, n=nil
12051247
end
12061248
alias :norm2 :nrm2
12071249

1250+
# Norm calculation methods
1251+
# Frobenius norm: the Euclidean norm of the matrix, treated as if it were a vector
1252+
def fro_matrix_norm
1253+
#float64 has to be used in any case, since nrm2 will not yield correct result for float32
1254+
self_cast = self.cast(:dtype => :float64)
1255+
1256+
column_vector = self_cast.reshape([self.size, 1])
1257+
1258+
return column_vector.nrm2
1259+
end
1260+
1261+
# 2-norm: the largest/smallest singular value of the matrix
1262+
def two_matrix_norm minus = false
1263+
1264+
self_cast = self.cast(:dtype => :float64)
1265+
1266+
#TODO: confirm if this is the desired svd calculation
1267+
svd = self_cast.gesvd
1268+
return svd[1][0, 0] unless minus
1269+
return svd[1][svd[1].rows-1, svd[1].cols-1]
1270+
end
1271+
1272+
# 1-norm: the maximum/minimum absolute column sum of the matrix
1273+
def one_matrix_norm minus = false
1274+
#TODO: change traversing method for sparse matrices
1275+
number_of_columns = self.cols
1276+
col_sums = []
1277+
1278+
number_of_columns.times do |i|
1279+
col_sums << self.col(i).inject(0) { |sum, number| sum += number.abs}
1280+
end
1281+
1282+
return col_sums.max unless minus
1283+
return col_sums.min
1284+
end
1285+
1286+
# Infinity norm: the maximum/minimum absolute row sum of the matrix
1287+
def inf_matrix_norm minus = false
1288+
number_of_rows = self.rows
1289+
row_sums = []
1290+
1291+
number_of_rows.times do |i|
1292+
row_sums << self.row(i).inject(0) { |sum, number| sum += number.abs}
1293+
end
1294+
1295+
return row_sums.max unless minus
1296+
return row_sums.min
1297+
end
1298+
12081299
#
12091300
# call-seq:
12101301
# scale! -> NMatrix
@@ -1458,4 +1549,4 @@ def dtype_for_floor_or_ceil
14581549
self.__dense_map__ { |l| l.send(op,rhs) }
14591550
end
14601551
end
1461-
end
1552+
end

spec/math_spec.rb

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,9 @@
462462

463463
it "should correctly invert a matrix in place (bang)" do
464464
pending("not yet implemented for :object dtype") if dtype == :object
465-
a = NMatrix.new(:dense, 5, [1, 8,-9, 7, 5,
466-
0, 1, 0, 4, 4,
467-
0, 0, 1, 2, 5,
465+
a = NMatrix.new(:dense, 5, [1, 8,-9, 7, 5,
466+
0, 1, 0, 4, 4,
467+
0, 0, 1, 2, 5,
468468
0, 0, 0, 1,-5,
469469
0, 0, 0, 0, 1 ], dtype)
470470
b = NMatrix.new(:dense, 5, [1,-8, 9, 7, 17,
@@ -1148,7 +1148,7 @@
11481148
3, 4, 5,
11491149
6, 7, 8], stype: stype, dtype: dtype)
11501150
end
1151-
1151+
11521152
it "scales the matrix by a given factor and return the result" do
11531153
pending("not yet implemented for :object dtype") if dtype == :object
11541154
if integer_dtype? dtype
@@ -1160,7 +1160,7 @@
11601160
12, 14, 16], stype: stype, dtype: dtype))
11611161
end
11621162
end
1163-
1163+
11641164
it "scales the matrix in place by a given factor" do
11651165
pending("not yet implemented for :object dtype") if dtype == :object
11661166
if dtype == :int8
@@ -1177,4 +1177,78 @@
11771177
end
11781178
end
11791179
end
1180+
context "matrix_norm" do
1181+
ALL_DTYPES.each do |dtype|
1182+
context dtype do
1183+
pending("not yet implemented for :object dtype") if dtype == :object
1184+
before do
1185+
@n = NMatrix.new([3,3], [-4,-3,-2,
1186+
-1, 0, 1,
1187+
2, 3, 4], dtype: dtype)
1188+
1189+
@matrix_norm_TOLERANCE = 1.0e-10
1190+
end
1191+
1192+
it "should default to 2-matrix_norm" do
1193+
pending("not yet implemented for NMatrix-JRuby") if jruby?
1194+
if(dtype == :byte)
1195+
expect{@n.matrix_norm}.to raise_error(ArgumentError)
1196+
else
1197+
begin
1198+
expect(@n.matrix_norm).to be_within(@matrix_norm_TOLERANCE).of(7.348469228349535)
1199+
1200+
rescue NotImplementedError
1201+
pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1202+
end
1203+
end
1204+
end
1205+
1206+
it "should reject invalid arguments" do
1207+
pending("not yet implemented for NMatrix-JRuby") if jruby?
1208+
1209+
expect{@n.matrix_norm(0.5)}.to raise_error(ArgumentError)
1210+
end
1211+
1212+
it "should calculate 1 and 2(minus) matrix_norms correctly" do
1213+
pending("not yet implemented for NMatrix-JRuby") if jruby?
1214+
if(dtype == :byte)
1215+
expect{@n.matrix_norm(1)}.to raise_error(ArgumentError)
1216+
expect{@n.matrix_norm(-2)}.to raise_error(ArgumentError)
1217+
expect{@n.matrix_norm(-1)}.to raise_error(ArgumentError)
1218+
else
1219+
expect(@n.matrix_norm(1)).to eq(7)
1220+
begin
1221+
1222+
#FIXME: change to the correct value when overflow issue is resolved
1223+
#expect(@n.matrix_norm(-2)).to eq(1.8628605857884395e-07)
1224+
expect(@n.matrix_norm(-2)).to be_within(@matrix_norm_TOLERANCE).of(0.0)
1225+
rescue NotImplementedError
1226+
pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1227+
end
1228+
expect(@n.matrix_norm(-1)).to eq(6)
1229+
end
1230+
end
1231+
1232+
it "should calculate infinity matrix_norms correctly" do
1233+
pending("not yet implemented for NMatrix-JRuby") if jruby?
1234+
if(dtype == :byte)
1235+
expect{@n.matrix_norm(:inf)}.to raise_error(ArgumentError)
1236+
expect{@n.matrix_norm(:'-inf')}.to raise_error(ArgumentError)
1237+
else
1238+
expect(@n.matrix_norm(:inf)).to eq(9)
1239+
expect(@n.matrix_norm(:'-inf')).to eq(2)
1240+
end
1241+
end
1242+
1243+
it "should calculate frobenius matrix_norms correctly" do
1244+
pending("not yet implemented for NMatrix-JRuby") if jruby?
1245+
if(dtype == :byte)
1246+
expect{@n.matrix_norm(:fro)}.to raise_error(ArgumentError)
1247+
else
1248+
expect(@n.matrix_norm(:fro)).to be_within(@matrix_norm_TOLERANCE).of(7.745966692414834)
1249+
end
1250+
end
1251+
end
1252+
end
1253+
end
11801254
end

0 commit comments

Comments
 (0)