The package jmatrix
(Domingo
(2023a)) was originally conceived as a tool for other packages,
namely parallelpam
(Domingo
(2023b)) and scellpam
(Domingo (2023c)) which needed to deal with very
big matrices which might not fit in the memory of the computer,
particularly if their elements are of type double
(in most
modern machines, 8 bytes per element) whereas they could fit if they
were matrices of other data types, in particular of floats (4 bytes per
element).
Unfortunately, R is not a strongly typed language. Double is the default type in R and it is not easy to work with other data types. Trials like the package float (Schmidt (2022)) have been done, but to use them you have to coerce a matrix already loaded in R memory to a float matrix, and then you can delete it. But, what happens if you computer has not memory enough to hold the matrix in the first place?. This is the problem this package tries to address.
Our idea is to use the disk as temporarily storage of the matrix in a
file with a internal binary format (jmatrix
format). This
format has a header of 128 bytes with information like type of matrix
(full, sparse or symmetric), data type of each element (char, short,
int, long, float, double or long double), number of rows and columns and
endianness; then comes the content as binary data (in sparse matrices
zeros are not stored; in symmetric matrices only the lower-diagonal is
stored) and finally the metadata (currently, names for rows/columns if
needed and an optional comment).
Such files are created and loaded by functions written in C++ which
are accessible from R
with Rcpp (Eddelbuettel and François (2011)). The file,
once loaded, uses strictly the needed memory for its data type and can
be processed by other C++ functions (like the PAM algorithm or any other
numeric library written in C++) also from inside R
.
The matrix contained in a binary data file in jmatrix
format cannot be loaded directly in R
memory as a
R
matrix (that would be impossible, anyway, since precisely
this package is done for the cases in which such matrix would NOT fit
into the available RAM). Nevertheless, limited access through some
functions is provided to read one or more rows or one or more columns as
R
vectors or matrices (obviously, coerced to double).
The package jmatrix
must not be considered as a final,
finished software. Currently is mostly an instrumental solution to
address our needs and we make available as a separate package just in
case it could be useful for anyone else.
First of all, the package can show quite informative (but sometimes verbose) messages in the console. To turn on/off such messages you can use.
As stated before, the binary matrix files should normally be created
from C++ getting the data from an external source like a data file in a
format used in bioinformatics or a .csv file. These files should be read
by chunks. As an example, look at function CsvToJMat
in
package scellpam
.
As a convenience and only for testing purposes (to be used in this
vignette), we provide the function JWriteBin
to write a R
matrix as a jmatrix
file.
# Create a 6x8 matrix of random values
Rf <- matrix(runif(48),nrow=6)
# Set row and column names for it
rownames(Rf) <- c("A","B","C","D","E","F")
colnames(Rf) <- c("a","b","c","d","e","f","g","h")
# Let's see the matrix
Rf
#> a b c d e f g
#> A 0.2053683 0.061604031 0.4408997 0.8256495 0.3250689 0.63942657 0.01609905
#> B 0.2350405 0.516145485 0.9008904 0.1989480 0.9445564 0.93459646 0.92401352
#> C 0.7958220 0.393336945 0.6957560 0.6374684 0.8606968 0.04649475 0.64416093
#> D 0.1879173 0.799869398 0.7513315 0.3154559 0.9665992 0.28000631 0.80159160
#> E 0.7339204 0.009680571 0.9970633 0.4304082 0.2835436 0.49277567 0.82760117
#> F 0.5321249 0.769233066 0.7962894 0.4943540 0.8797848 0.83118090 0.22414134
#> h
#> A 0.4695142
#> B 0.8641575
#> C 0.7710607
#> D 0.4702106
#> E 0.8642866
#> F 0.7142479
# and write it as the binary file Rfullfloat.bin
JWriteBin(Rf,"Rfullfloat.bin",dtype="float",dmtype="full",
comment="Full matrix of floats")
#> The passed matrix has row names for the 6 rows and they will be used.
#> The passed matrix has column names for the 8 columns and they will be used.
#> Writing binary matrix Rfullfloat.bin of (6x8)
#> End of block of binary data at offset 320
#> Writing row names (6 strings written, from A to F).
#> Writing column names (8 strings written, from a to h).
#> Writing comment: Full matrix of floats
# Also, you can write it with double data type:
JWriteBin(Rf,"Rfulldouble.bin",dtype="double",dmtype="full",
comment="Full matrix of doubles")
#> The passed matrix has row names for the 6 rows and they will be used.
#> The passed matrix has column names for the 8 columns and they will be used.
#> Writing binary matrix Rfulldouble.bin of (6x8)
#> End of block of binary data at offset 512
#> Writing row names (6 strings written, from A to F).
#> Writing column names (8 strings written, from a to h).
#> Writing comment: Full matrix of doubles
To get information about the stored file the function
JMatInfo
is provided. Of course, this funcion does not read
the complete file in memory but just the header.
# Information about the float binary file
JMatInfo("Rfullfloat.bin")
#> File: Rfullfloat.bin
#> Matrix type: FullMatrix
#> Number of elements: 48
#> Data type: float
#> Endianness: little endian (same as this machine)
#> Number of rows: 6
#> Number of columns: 8
#> Metadata: Stored names of rows and columns.
#> Metadata comment: "Full matrix of floats"
# Same information about the double binary file
JMatInfo("Rfulldouble.bin")
#> File: Rfulldouble.bin
#> Matrix type: FullMatrix
#> Number of elements: 48
#> Data type: double
#> Endianness: little endian (same as this machine)
#> Number of rows: 6
#> Number of columns: 8
#> Metadata: Stored names of rows and columns.
#> Metadata comment: "Full matrix of doubles"
A jmatrix binary file can be exported to .csv/.tsv table. This is
done with the function JMatToCsv
# Create a 6x8 matrix of random values
Rf <- matrix(runif(48),nrow=6)
# Set row and column names for it
rownames(Rf) <- c("A","B","C","D","E","F")
colnames(Rf) <- c("a","b","c","d","e","f","g","h")
# Store it as the binary file Rfullfloat.bin
JWriteBin(Rf,"Rfullfloat.bin",dtype="float",dmtype="full",
comment="Full matrix of floats")
#> The passed matrix has row names for the 6 rows and they will be used.
#> The passed matrix has column names for the 8 columns and they will be used.
#> Writing binary matrix Rfullfloat.bin of (6x8)
#> End of block of binary data at offset 320
#> Writing row names (6 strings written, from A to F).
#> Writing column names (8 strings written, from a to h).
#> Writing comment: Full matrix of floats
# Save the content of this .bin as a .csv file
JMatToCsv("Rfullfloat.bin","Rfullfloat.csv",csep=",",withquotes=FALSE)
#> Read full matrix with size (6,8)
The generated file will not have quotes neither around the column names (in its first line) nor around each row name (at the beginning of each line) since withquotes is FALSE but it can be set to TRUE for the opposite behavior. Also, a .tsv (tabulator separated values) would have been generated using csep=“\t”.
Also, a jmatrix binary file can also be generated from a .csv/.tsv
file. Such file must have a first line with the names of the columns
(possibly surrounded by double quotes, including a first empty
double-quote, since the column of row names has no name itself). The
rest of its lines must start with a string (possibly surrounded by
double quotes) with the row name and the values. In all cases (first
line and data lines) each column must be separated from the next by a
separation character (usually, a comma). No separation character must be
added at the end of each line. This format is compatible with the .csv
generated by R with the function write.csv
.
The function to read .csv files is CsvToJMat
# Create a 6x8 matrix of random values
Rf <- matrix(runif(48),nrow=6)
# Set row and column names for it
rownames(Rf) <- c("A","B","C","D","E","F")
colnames(Rf) <- c("a","b","c","d","e","f","g","h")
# Save it as a .csv file with the standard R function...
write.csv(Rf,"rf.csv")
# ...and read it to create a jmatrix binary file
CsvToJMat("rf.csv","rf.bin",mtype="full",csep=",",ctype="raw",valuetype="float",transpose=FALSE,comment="Test matrix generated reading a .csv file")
#> 8 columns of values (not including the column of names) in file rf.csv.
#> 6 lines (excluding header) in file rf.csv
#> Data will be read from each line and stored as float values.
#> Reading line... 0
#> Read 6 data lines of file rf.csv, as expected.
#> Writing binary matrix rf.bin of (6x8)
#> End of block of binary data at offset 320
#> Writing row names (6 strings written, from A to F).
#> Writing column names (8 strings written, from a to h).
#> Writing comment: Test matrix generated reading a .csv file
# Let's see the characteristics of the binary file
JMatInfo("rf.bin")
#> File: rf.bin
#> Matrix type: FullMatrix
#> Number of elements: 48
#> Data type: float
#> Endianness: little endian (same as this machine)
#> Number of rows: 6
#> Number of columns: 8
#> Metadata: Stored names of rows and columns.
#> Metadata comment: "Test matrix generated reading a .csv file"
The parameter mtype=“symmetric” will consider the content of the .csv file as a symmetric matrix. This implies that it must be a square matrix (same number of rows and columns) but the upper-diagonal matrix that must be present (it does not matter with which values) will be read, and immediately ignored, i.e.: only the lower-diagonal matrix (including the main diagonal) will be stored.
As stated before, no function is provided to read the whole matrix in memory which would contradict the philosophy of this package, but you can get rows or columns from a file.
# Reads row 1 into vector vf. Float values inside the file are
# promoted to double.
(vf<-GetJRow("Rfullfloat.bin",1))
#> a b c d e f g
#> 0.20583504 0.66527879 0.55852747 0.66488999 0.79858172 0.73632532 0.04402424
#> h
#> 0.06579623
Obviously, storage in float provokes a loosing of precision. We have
observed this not to be relevant for PAM
(partitioning
around medoids) algorihm but it can be important in other cases. It is
the price to pay for halving the needed space.
Nevertheless, storing as double obviously keeps the data intact.
Now, let us see examples of some functions to read rows or columns by number or by name, or to read several rows/columns as a R matrix. In all examples numbers for rows and columns are in R-convention (i.e. starting at 1)
# Read column number 3
(vf<-GetJCol("Rfullfloat.bin",3))
#> A B C D E F
#> 0.55852747 0.07322597 0.60049063 0.87614644 0.50686646 0.11414684
# Test precision
max(abs(Rf[,3]-vf))
#> [1] 0.7073944
# Read row with name C
(vf<-GetJRowByName("Rfullfloat.bin","C"))
#> a b c d e f g h
#> 0.8293205 0.3407052 0.6004906 0.4491243 0.9340876 0.7261943 0.8495708 0.8644984
# Read column with name c
(vf<-GetJColByName("Rfullfloat.bin","c"))
#> A B C D E F
#> 0.55852747 0.07322597 0.60049063 0.87614644 0.50686646 0.11414684
# Get the names of all rows or columns as vectors of R strings
(rn<-GetJRowNames("Rfullfloat.bin"))
#> [1] "A" "B" "C" "D" "E" "F"
(cn<-GetJColNames("Rfullfloat.bin"))
#> [1] "a" "b" "c" "d" "e" "f" "g" "h"
# Get the names of rows and columns simultaneosuly as a list of two elements
(l<-GetJNames("Rfullfloat.bin"))
#> $rownames
#> [1] "A" "B" "C" "D" "E" "F"
#>
#> $colnames
#> [1] "a" "b" "c" "d" "e" "f" "g" "h"
# Get several rows at once. The returned matrix has the rows in the
# same order as the passed list,
# and this list can contain even repeated values
(vm<-GetJManyRows("Rfullfloat.bin",c(1,4)))
#> a b c d e f g
#> A 0.2058350 0.6652788 0.5585275 0.6648900 0.7985817 0.7363253 0.04402424
#> D 0.1370095 0.4629888 0.8761464 0.0653004 0.6311588 0.1865624 0.73003858
#> h
#> A 0.06579623
#> D 0.33107263
# Of course, columns can be extrated equally
(vc<-GetJManyCols("Rfulldouble.bin",c(1,4)))
#> a d
#> A 0.2053683 0.8256495
#> B 0.2350405 0.1989480
#> C 0.7958220 0.6374684
#> D 0.1879173 0.3154559
#> E 0.7339204 0.4304082
#> F 0.5321249 0.4943540
# and similar functions are provided for extracting by names:
(vm<-GetJManyRowsByNames("Rfulldouble.bin",c("A","D")))
#> a b c d e f g
#> A 0.2053683 0.06160403 0.4408997 0.8256495 0.3250689 0.6394266 0.01609905
#> D 0.1879173 0.79986940 0.7513315 0.3154559 0.9665992 0.2800063 0.80159160
#> h
#> A 0.4695142
#> D 0.4702106
(vc<-GetJManyColsByNames("Rfulldouble.bin",c("a","d")))
#> a d
#> A 0.2053683 0.8256495
#> B 0.2350405 0.1989480
#> C 0.7958220 0.6374684
#> D 0.1879173 0.3154559
#> E 0.7339204 0.4304082
#> F 0.5321249 0.4943540
The package can manage and store sparse and symmetric matrices, too.
# Generation of a 6x8 sparse matrix
Rsp <- matrix(rep(0,48),nrow=6)
sparsity <- 0.1
nnz <- round(48*sparsity)
where <- floor(47*runif(nnz))
val <- runif(nnz)
for (i in 1:nnz)
{
Rsp[floor(where[i]/8)+1,(where[i]%%8)+1] <- val[i]
}
rownames(Rsp) <- c("A","B","C","D","E","F")
colnames(Rsp) <- c("a","b","c","d","e","f","g","h")
# Let's see the matrix
Rsp
#> a b c d e f g h
#> A 0 0.00000 0 0 0 0.0000000 0.0000000 0.4395743
#> B 0 0.00000 0 0 0 0.0000000 0.0000000 0.0000000
#> C 0 0.00000 0 0 0 0.1723219 0.0000000 0.0000000
#> D 0 0.00000 0 0 0 0.0000000 0.0000000 0.0000000
#> E 0 0.47116 0 0 0 0.0000000 0.0000000 0.0000000
#> F 0 0.00000 0 0 0 0.0000000 0.6929998 0.0000000
# Write the matrix as sparse with type float
JWriteBin(Rsp,"Rspafloat.bin",dtype="float",dmtype="sparse",
comment="Sparse matrix of floats")
#> The passed matrix has row names for the 6 rows and they will be used.
#> The passed matrix has column names for the 8 columns and they will be used.
#> Writing binary matrix Rspafloat.bin of (6x8)
#> End of block of binary data at offset 184
#> Writing row names (6 strings written, from A to F).
#> Writing column names (8 strings written, from a to h).
#> Writing comment: Sparse matrix of floats
Notice that the condition of being a sparse matrix and the storage space used can be known with the matrix info.
JMatInfo("Rspafloat.bin")
#> File: Rspafloat.bin
#> Matrix type: SparseMatrix
#> Number of elements: 48
#> Data type: float
#> Endianness: little endian (same as this machine)
#> Number of rows: 6
#> Number of columns: 8
#> Metadata: Stored names of rows and columns.
#> Metadata comment: "Sparse matrix of floats"
#> Binary data size: 56 bytes, which is 29.1667% of the full matrix size (which would be 192 bytes).
Be careful: trying to store as sparse a matrix which is not (it has not a majority of 0-entries) works, but produces a matrix larger than the corresponding full matrix.
With respect to symmetric matrices, JWriteBin
works the
same way. Let us generate a 7 × 7
symmetric matrix.
Rns <- matrix(runif(49),nrow=7)
Rsym <- 0.5*(Rns+t(Rns))
rownames(Rsym) <- c("A","B","C","D","E","F","G")
colnames(Rsym) <- c("a","b","c","d","e","f","g")
# Let's see the matrix
Rsym
#> a b c d e f g
#> A 0.5321636 0.44397435 0.6939942 0.2646135 0.5320929 0.5126158 0.76414389
#> B 0.4439744 0.04774475 0.3372440 0.4242766 0.7534189 0.2055427 0.52497979
#> C 0.6939942 0.33724402 0.6068064 0.3850373 0.7655085 0.3101850 0.38245740
#> D 0.2646135 0.42427662 0.3850373 0.7046735 0.9268014 0.3372944 0.51701699
#> E 0.5320929 0.75341891 0.7655085 0.9268014 0.2298873 0.4850499 0.59460675
#> F 0.5126158 0.20554274 0.3101850 0.3372944 0.4850499 0.8987369 0.39183867
#> G 0.7641439 0.52497979 0.3824574 0.5170170 0.5946067 0.3918387 0.01148575
# Write the matrix as symmetric with type float
JWriteBin(Rsym,"Rsymfloat.bin",dtype="float",dmtype="symmetric",
comment="Symmetric matrix of floats")
#> The passed matrix has row names for the 7 rows and they will be used.
#> Writing binary matrix Rsymfloat.bin
#> End of block of binary data at offset 240
#> Writing row names (7 strings written, from A to G).
#> Writing comment: Symmetric matrix of floats
# Get the information
JMatInfo("Rsymfloat.bin")
#> File: Rsymfloat.bin
#> Matrix type: SymmetricMatrix
#> Number of elements: 49 (28 really stored)
#> Data type: float
#> Endianness: little endian (same as this machine)
#> Number of rows: 7
#> Number of columns: 7
#> Metadata: Stored only names of rows.
#> Metadata comment: "Symmetric matrix of floats"
Notice that if you store a R matrix which is NOT symmetric as a
symmetric jmatrix
, only the lower triangular part
(including the main diagonal) will be saved. The upper-triangular part
will be lost.
The functions to read rows/colums stated before works equally
independently of the matrix character (full, sparse or symmetric) so you
can play with them using the Rspafloat.bin
and
Rsymfloat.bin
file to check they work.
Finally, if the jmatrix stored in a binary file has names associated to rows or columns, you can filter it using them and generate another jmatrix file with only the rows or columns you wish to keep. The function to do so is ‘FilterJMatByName’.
Rns <- matrix(runif(49),nrow=7)
rownames(Rns) <- c("A","B","C","D","E","F","G")
colnames(Rns) <- c("a","b","c","d","e","f","g")
# Let's see the matrix
Rns
#> a b c d e f g
#> A 0.4207025 0.019846637 0.91300943 0.07905621 0.96660048 0.7775585 0.8266334
#> B 0.5699335 0.005125603 0.50792848 0.47087736 0.91334684 0.6083844 0.6238567
#> C 0.3908808 0.807726296 0.77137927 0.79076152 0.37798291 0.2322851 0.7691913
#> D 0.5018790 0.592217654 0.98197560 0.03744208 0.11753846 0.5955644 0.8727684
#> E 0.2609108 0.034073545 0.92714133 0.67778665 0.79363964 0.3013692 0.5180521
#> F 0.7784398 0.868210976 0.04881504 0.51402340 0.06501864 0.9754264 0.3212875
#> G 0.2894746 0.963725767 0.59037255 0.45462300 0.34819486 0.6475272 0.1205930
# Write the matrix as full with type float
JWriteBin(Rns,"Rfullfloat.bin",dtype="float",dmtype="full",
comment="Full matrix of floats")
#> The passed matrix has row names for the 7 rows and they will be used.
#> The passed matrix has column names for the 7 columns and they will be used.
#> Writing binary matrix Rfullfloat.bin of (7x7)
#> End of block of binary data at offset 324
#> Writing row names (7 strings written, from A to G).
#> Writing column names (7 strings written, from a to g).
#> Writing comment: Full matrix of floats
# Extract the first two and the last two columns
FilterJMatByName("Rfullfloat.bin",c("a","b","f","g"),"Rfullfloat_fourcolumns.bin",namesat="cols")
#> Read full matrix with size (7,7)
#> Writing binary matrix Rfullfloat_fourcolumns.bin of (7x4)
#> End of block of binary data at offset 240
#> Writing row names (7 strings written, from A to G).
#> Writing column names (4 strings written, from a to g).
#> Writing comment: Full matrix of floats
# Let's load the matrix and let's see it
vm<-GetJManyRows("Rfullfloat_fourcolumns.bin",c(1,7))
vm
#> a b f g
#> A 0.4207025 0.01984664 0.7775585 0.8266334
#> G 0.2894746 0.96372575 0.6475272 0.1205930