tatami
C++ API for different matrix representations

tatami is a spiritual successor to the beachmat C++ API that provides read access to different matrix representations. Specifically, applications can use tatami to read rows and/or columns of a matrix without any knowledge of the specific matrix representation. This allows application developers to write a single piece of code that will work seamlessly with different inputs, even if the underlying representation varies at runtime. In particular, tatami is motivated by analyses of processed genomics data, where matrices are often interpreted as a collection of row or columnwise vectors. Many applications involve looping over rows or columns to compute some statistic or summary  for example, testing for differential expression within each row of the matrix. tatami aims to optimize this access pattern across a variety of different matrix representations, depending on how the data is provided to the application.
tatami is a headeronly library, so it can be easily used by just #include
ing the relevant source files:
The key idea here is that, once mat
is created, the application does not need to worry about the exact format of the matrix referenced by the pointer. Application developers can write code that works interchangeably with a variety of different matrix representations.
tatami::Matrix
Users can create an instance of a concrete tatami::Matrix
subclass by using one of the constructors or the equivalent make_*
utility:
Description  Class or function 

Dense matrix  DenseMatrix 
Compressed sparse matrix  CompressedSparseMatrix 
List of lists sparse matrix  FragmentedSparseMatrix 
Delayed isometric unary operation  make_DelayedUnaryIsometricOperation() 
Delayed isometric binary operation  make_DelayedBinaryIsometricOperation() 
Delayed combination  make_DelayedBind() 
Delayed subset  make_DelayedSubset() 
Delayed transpose  make_DelayedTranspose() 
Delayed cast  make_DelayedCast() 
For example, to create a compressed sparse matrix from sparse triplet data (x
, i
, j
), we could do:
We typically create a shared_ptr
to a tatami::Matrix
to leverage runtime polymorphism. This enables downstream applications to accept many different matrix representations by compiling against the tatami::Matrix
interface. Alternatively, applications may use templating to achieve compiletime polymorphism on the different tatami subclasses, but this is rather restrictive without providing obvious performance benefits.
We use templating to define the type of values returned by the interface. This includes the type of the data (most typically double
) as well as the type of row/column indices (default int
, but one could imagine using, e.g., size_t
). It is worth noting that the storage type does not need to be the same as the interface type. For example, developers could store a matrix of small counts as uint16_t
while returning double
s for compatibility with downstream mathematical code.
The delayed operations are ~stolen from~ inspired by those in the DelayedArray package. Isometric operations are particularly useful as they accommodate matrixscalar/vector arithmetic and various mathematical operations. For example, we could apply a sparsitybreaking delayed operation to our sparse matrix mat
without actually creating a dense matrix:
Some libraries in the @tatamiinc organization implement further extensions of tatami's interface, e.g., for HDF5backed matrices and Rbased matrices.
Given an abstract tatami::Matrix
, we create an Extractor
instance to actually extract the matrix data. Each Extractor
object can store intermediate data for reuse during iteration through the matrix, which is helpful for some matrix implementations that do not easily support random access. For example, to perform extract dense rows from our mat
:
The tatami::MyopicDenseExtractor::fetch()
method returns a pointer to the row's contents. In some matrix representations (e.g., DenseMatrix
), the returned pointer directly refers to the matrix's internal data store. However, this is not the case in general so we need to allocate a buffer of appropriate length (buffer
) in which the dense contents can be stored; if this buffer is used, the returned pointer refers to the start of the buffer.
Alternatively, we could extract sparse columns via tatami::MyopicSparseExtractor::fetch()
, which returns a tatami::SparseRange
containing pointers to arrays of (structurally nonzero) values and their row indices. This provides some opportunities for optimization in algorithms that only need to operate on nonzero values. The fetch()
call requires buffers for both arrays  again, this may not be used for matrix subclasses with contiguous internal storage of the values/indices.
In both the dense and sparse cases, we can restrict the values that are extracted by fetch()
. This provides some opportunities for optimization by avoiding the unnecessary extraction of uninteresting data. To do so, we specify the start and length of a contiguous block of interest, or we supply a vector containing the indices of the elements of interest:
In performancecritical sections, it may be desirable to customize the extraction based on properties of the matrix. This is supported with the following methods:
tatami::Matrix::is_sparse()
indicates whether a matrix is sparse.tatami::Matrix::prefer_rows()
indicates whether a matrix is more efficiently accessed along its rows (e.g., rowmajor dense matrices).Users can then write dedicated code paths to take advantage of these properties. For example, we might use different algorithms for dense data, where we don't have to look up indices; and for sparse data, if we can avoid the uninteresting zero values. Similarly, if we want to compute a rowwise statistic, but the matrix is more efficiently accessed by column according to prefer_rows()
, we could iterate on the columns and attempt to compute the statistic in a "running" manner (see colsums.cpp
for an example). In the most complex cases, this leads to code like:
Of course, this assumes that it is possible to provide sparsespecific optimizations as well as running calculations for the statistic of interest. In most cases, only a subset of the extraction patterns are actually feasible so special code paths would not be beneficial.
The mutable nature of an Extractor
instance means that the fetch()
calls themselves are not const
. This means that the same extractor cannot be safely reused across different threads as each call to fetch()
will modify the extractor's contents. Fortunately, the solution is simple  just create a separate Extractor
(and the associated buffers) for each thread. With OpenMP, this looks like:
Users may also consider using the tatami::parallelize()
function, which accepts a function with the range of jobs (in this case, rows) to be processed in each thread. This automatically falls back to the standard <thread>
library if OpenMP is not available. Applications can also set the TATAMI_CUSTOM_PARALLEL
macro to override the parallelization scheme in all tatami::parallelize()
calls.
When constructing an Extractor
, users can supply an Oracle
that specifies the sequence of rows/columns to be accessed. Knowledge of the future access pattern enables optimizations in some Matrix
implementations, e.g., filebacked matrices can reduce the number of disk reads by prefetching the right data for future accesses. The most obvious use case involves accessing consecutive rows/columns:
In fact, this use case is so common that we can just use the tatami::consecutive_extractor()
wrapper to construct the oracle and pass it to tatami::Matrix
. This will return a tatami::OracularDenseExtractor
instance that contains the oracle's predictions.
Alternatively, we can use the FixedOracle
class with an array of row/column indices that are known ahead of time. Advanced users can also define their own Oracle
subclasses to generate predictions on the fly.
As previously mentioned, tatami is designed to pull out rows or columns of a matrix, and little else. Some support is provided for basic statistics in the same vein as the matrixStats R package  see the tatami_stats library for more information.
tatami does not directly support matrix algebra or decompositions. If these highlevel operations are needed, applications should write their own code, e.g., by using tatami's extractors to implement matrix multiplication. Alternatively, we can transfer data from tatami into other frameworks like Eigen for complex matrix operations, effectively trading the diversity of representations for a more comprehensive suite of operations. For example, we often use tatami to represent the input data in a custom format to save memory for large datasets; process it into a much smaller submatrix, e.g., by selecting features of interest in a genomescale analysis; and then copy this cheaply into an Eigen::MatrixXd
or Eigen::SparseMatrix
for more computationally intensive work.
It is not possible to modify the matrix contents via the tatami API. This is especially relevant for matrices with delayed operations or those referring to remote data stores, where reading the matrix data is trivial but writing is not guaranteed to work. Experience suggests that a matrix writer abstraction is less useful than the equivalent reader abstraction. This is because applications typically control the output format, so there is no need to accommodate a diversity of formats via an abstract interface.
FetchContent
If you're using CMake, you just need to add something like this to your CMakeLists.txt
:
Then you can link to tatami to make the headers available during compilation:
find_package()
You can install the library by cloning a suitable version of this repository and running the following commands:
Then you can use find_package()
as usual:
By default, this will use FetchContent
to fetch all external dependencies. If you want to install them manually, use DTATAMI_FETCH_EXTERN=OFF
. See extern/CMakeLists.txt
to find compatible versions of each dependency.
If you're not using CMake, the simple approach is to just copy the files the include/
subdirectory  either directly or with Git submodules  and include their path during compilation with, e.g., GCC's I
. The external dependencies listed in extern/CMakeLists.txt
also need to be made available during compilation.
Check out the reference documentation for more details on each function and class.
The gallery also contains worked examples for common operations based on row/column traversals.
The tatami_stats repository computes some common statistics on tatami matrices.
The tatami_hdf5 repository contains tatami bindings for HDF5backed matrices.
The tatami_r repository contains tatami bindings for matrixlike objects in R.
The beachmat package vendors the tatami headers for easy use by other R packages.