faissR: nearest neighbours, graphs, and clustering

Overview

faissR provides native nearest-neighbour search, graph construction, graph clustering, k-nearest-neighbour prediction, and k-means utilities for R workflows that use large row-wise matrices. The package is designed for high-throughput biological data analysis, including single-cell, flow cytometry, imaging, and mass spectrometry applications.

The package requires the FAISS C++ library for CPU indexes. CUDA, FAISS GPU indexes, NVIDIA cuVS, and RAPIDS libcugraph are optional. CPU-only installations do not need NVIDIA libraries, and explicit CUDA requests fail clearly when CUDA support is not available.

The main public functions are:

Function Purpose
nn() nearest-neighbour search on host matrices
nn_gpu() GPU-resident exact-family KNN result for downstream CUDA code
candidate_knn() refine a supplied candidate neighbour set
knn() and predict() supervised kNN fitting and prediction
knn_graph() construct a weighted nearest-neighbour graph
graph_cluster() random-walking, Louvain, or Leiden-style graph clustering
fast_kmeans() CPU or optional CUDA k-means
backend_info() and nn_capabilities() inspect compiled backends

Example data

The examples use a small synthetic matrix with three groups. Rows are observations and columns are features, matching the input convention used by faissR.

library(faissR)

set.seed(100)
n_per_group <- 30L
p <- 8L

centers <- rbind(
    rep(-2, p),
    rep(0, p),
    rep(2, p)
)

x <- do.call(rbind, lapply(seq_len(nrow(centers)), function(i) {
    sweep(
        matrix(rnorm(n_per_group * p, sd = 0.7), ncol = p),
        2,
        centers[i, ],
        "+"
    )
}))

groups <- factor(rep(LETTERS[1:3], each = n_per_group))
dim(x)
#> [1] 90  8
table(groups)
#> groups
#>  A  B  C 
#> 30 30 30

Backends

backend_info() reports which compiled routes are available in the current R session. On a CPU-only machine, CUDA-related entries are expected to be unavailable.

backend_info()[, c("backend", "available", "knn_available")]
#>          backend available knn_available
#> 1            cpu      TRUE          TRUE
#> 2          faiss      TRUE          TRUE
#> 3 faiss_gpu_cuvs     FALSE         FALSE
#> 4           cuvs     FALSE         FALSE
#> 5           cuda     FALSE         FALSE
#> 6        cugraph     FALSE         FALSE

nn_capabilities() gives method, backend, and metric support. The table can be large, so the example below shows only CPU Euclidean rows.

caps <- nn_capabilities()
subset(
    caps,
    backend == "cpu" & metric == "euclidean",
    select = c("method", "backend", "metric", "supported", "exact")
)
#>             method backend    metric supported exact
#> 5             auto     cpu euclidean      TRUE    NA
#> 17           exact     cpu euclidean      TRUE  TRUE
#> 29            flat     cpu euclidean      TRUE  TRUE
#> 41      bruteforce     cpu euclidean      TRUE  TRUE
#> 53            grid     cpu euclidean      TRUE  TRUE
#> 65            hnsw     cpu euclidean      TRUE FALSE
#> 77             ivf     cpu euclidean      TRUE FALSE
#> 89           ivfpq     cpu euclidean      TRUE FALSE
#> 101         vamana     cpu euclidean      TRUE FALSE
#> 113            nsg     cpu euclidean      TRUE FALSE
#> 125      nndescent     cpu euclidean      TRUE FALSE
#> 137 ivfpq_fastscan     cpu euclidean      TRUE FALSE
#> 149          cagra     cpu euclidean     FALSE    NA

Metrics

The public metric set is intentionally small:

  • euclidean;
  • cosine;
  • correlation;
  • inner_product.

Cosine normalizes each row before searching. Correlation first centers each row and then applies row normalization. Inner product ranks neighbours by larger raw dot product, while returned distances keep faissR’s smaller-is-better convention.

metric_results <- lapply(
    c("euclidean", "cosine", "correlation", "inner_product"),
    function(metric) {
        out <- nn(
            x,
            k = 5,
            backend = "cpu",
            method = "exact",
            metric = metric,
            exclude_self = TRUE,
            n_threads = 2
        )
        data.frame(
            metric = metric,
            backend_used = attr(out, "backend_used"),
            first_distance = round(out$distances[1, 1], 4)
        )
    }
)

do.call(rbind, metric_results)
#>          metric           backend_used first_distance
#> 1     euclidean          faiss_flat_l2         1.1514
#> 2        cosine      faiss_flat_cosine         0.0173
#> 3   correlation faiss_flat_correlation         0.2154
#> 4 inner_product          faiss_flat_ip         0.0000

Float32 input and output

FAISS and cuVS consume float32 data internally. Ordinary R numeric matrices are converted once for those compiled routes. If the optional float package is installed, faissR can also accept float::fl() input and return float32 distance matrices with output = "float".

xf <- float::fl(x)

float_nearest <- nn(
    xf,
    k = 5,
    backend = "cpu",
    method = "exact",
    metric = "euclidean",
    exclude_self = TRUE,
    output = "float",
    n_threads = 2
)

c(
    input_type = attr(float_nearest, "input_type"),
    distance_type = attr(float_nearest, "distance_type")
)
#>    input_type distance_type 
#>     "float32"     "float32"
class(float_nearest$distances)
#> [1] "float32"
#> attr(,"package")
#> [1] "float"

Reusing fitted indexes with kNN models

knn() can fit a model for repeated prediction, or it can fit and predict in one call when Xtest is supplied. Fitted FAISS Flat, HNSW, IVF, and IVFPQ indexes are reused by predict() where supported.

fit <- knn(
    Xtrain = x,
    Ytrain = groups,
    k = 7,
    backend = "cpu",
    method = "exact",
    metric = "euclidean",
    n_threads = 2
)

pred <- predict(fit, x[1:6, , drop = FALSE])
prob <- predict(fit, x[1:6, , drop = FALSE], type = "prob")

pred
#> [1] A A A A A A
#> attr(,"faissR_nn")
#> attr(,"faissR_nn")$k
#> [1] 7
#> 
#> attr(,"faissR_nn")$requested_backend
#> [1] auto
#> 
#> attr(,"faissR_nn")$requested_method
#> [1] exact
#> 
#> attr(,"faissR_nn")$tuning
#> [1] auto
#> 
#> attr(,"faissR_nn")$target_recall
#> [1] 0.99
#> 
#> attr(,"faissR_nn")$cagra_implementation
#> [1] <NA>
#> 
#> attr(,"faissR_nn")$cagra_build_algo
#> [1] <NA>
#> 
#> attr(,"faissR_nn")$backend
#> [1] faiss_flat_l2
#> 
#> attr(,"faissR_nn")$resolved_backend
#> [1] faiss_flat_l2
#> 
#> attr(,"faissR_nn")$metric
#> [1] euclidean
#> 
#> attr(,"faissR_nn")$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation
#> attr(,"faissR_nn")$approximation$strategy
#> [1] faiss_IndexFlatL2
#> 
#> attr(,"faissR_nn")$approximation$backend
#> [1] faiss_flat_l2
#> 
#> attr(,"faissR_nn")$approximation$library
#> [1] faiss
#> 
#> attr(,"faissR_nn")$approximation$metric
#> [1] euclidean
#> 
#> attr(,"faissR_nn")$approximation$input_type
#> [1] float32
#> 
#> attr(,"faissR_nn")$approximation$fitted_index
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$index_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$index_type
#> [1] IndexFlatL2ExternalPtr
#> 
#> attr(,"faissR_nn")$approximation$tuning_source
#> [1] none
#> 
#> attr(,"faissR_nn")$approximation$tuning_source
#> [1] none
#> 
#> attr(,"faissR_nn")$approximation$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$approximation$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$approximation$query_source
#> [1] fitted_index
#> 
#> 
#> attr(,"faissR_nn")$faiss
#> attr(,"faissR_nn")$faiss$index_type
#> [1] IndexFlatL2ExternalPtr
#> 
#> attr(,"faissR_nn")$faiss$backend
#> [1] cpu
#> 
#> attr(,"faissR_nn")$faiss$library
#> [1] faiss
#> 
#> attr(,"faissR_nn")$faiss$metric
#> [1] euclidean
#> 
#> attr(,"faissR_nn")$faiss$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_trained
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_training_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$build_train_call_count
#> [1] 0
#> 
#> attr(,"faissR_nn")$faiss$search_train_call_count
#> [1] 0
#> 
#> attr(,"faissR_nn")$faiss$vectors_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$faiss$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$faiss$query_source
#> [1] fitted_index
#> 
#> 
#> attr(,"faissR_nn")$cuvs
#> NULL
#> 
#> attr(,"faissR_nn")$spatial_index
#> NULL
#> 
#> attr(,"faissR_nn")$auto_selection
#> NULL
#> 
#> attr(,"faissR_nn")$metric_transform
#> NULL
#> 
#> attr(,"faissR_nn")$distance_transform
#> NULL
#> 
#> attr(,"faissR_nn")$input_type
#> [1] float32
#> 
#> attr(,"faissR_nn")$input_layout
#> [1] r_double_column_major_to_row_major_float32;fitted_index_query:r_double_column_major_to_row_major_float32
#> 
#> attr(,"faissR_nn")$input_owns_data
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$float32_compatibility_conversion
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$distance_type
#> [1] double
#> 
#> attr(,"faissR_nn")$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$query_source
#> [1] fitted_index
#> 
#> Levels: A B C
round(prob, 3)
#>      A B C
#> [1,] 1 0 0
#> [2,] 1 0 0
#> [3,] 1 0 0
#> [4,] 1 0 0
#> [5,] 1 0 0
#> [6,] 1 0 0
#> attr(,"faissR_nn")
#> attr(,"faissR_nn")$k
#> [1] 7
#> 
#> attr(,"faissR_nn")$requested_backend
#> [1] "auto"
#> 
#> attr(,"faissR_nn")$requested_method
#> [1] "exact"
#> 
#> attr(,"faissR_nn")$tuning
#> [1] "auto"
#> 
#> attr(,"faissR_nn")$target_recall
#> [1] 0.99
#> 
#> attr(,"faissR_nn")$cagra_implementation
#> [1] NA
#> 
#> attr(,"faissR_nn")$cagra_build_algo
#> [1] NA
#> 
#> attr(,"faissR_nn")$backend
#> [1] "faiss_flat_l2"
#> 
#> attr(,"faissR_nn")$resolved_backend
#> [1] "faiss_flat_l2"
#> 
#> attr(,"faissR_nn")$metric
#> [1] "euclidean"
#> 
#> attr(,"faissR_nn")$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation
#> attr(,"faissR_nn")$approximation$strategy
#> [1] "faiss_IndexFlatL2"
#> 
#> attr(,"faissR_nn")$approximation$backend
#> [1] "faiss_flat_l2"
#> 
#> attr(,"faissR_nn")$approximation$library
#> [1] "faiss"
#> 
#> attr(,"faissR_nn")$approximation$metric
#> [1] "euclidean"
#> 
#> attr(,"faissR_nn")$approximation$input_type
#> [1] "float32"
#> 
#> attr(,"faissR_nn")$approximation$fitted_index
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$index_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$index_type
#> [1] "IndexFlatL2ExternalPtr"
#> 
#> attr(,"faissR_nn")$approximation$tuning_source
#> [1] "none"
#> 
#> attr(,"faissR_nn")$approximation$tuning_source
#> [1] "none"
#> 
#> attr(,"faissR_nn")$approximation$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$approximation$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$approximation$query_source
#> [1] "fitted_index"
#> 
#> 
#> attr(,"faissR_nn")$faiss
#> attr(,"faissR_nn")$faiss$index_type
#> [1] "IndexFlatL2ExternalPtr"
#> 
#> attr(,"faissR_nn")$faiss$backend
#> [1] "cpu"
#> 
#> attr(,"faissR_nn")$faiss$library
#> [1] "faiss"
#> 
#> attr(,"faissR_nn")$faiss$metric
#> [1] "euclidean"
#> 
#> attr(,"faissR_nn")$faiss$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_trained
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_training_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$build_train_call_count
#> [1] 0
#> 
#> attr(,"faissR_nn")$faiss$search_train_call_count
#> [1] 0
#> 
#> attr(,"faissR_nn")$faiss$vectors_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$faiss$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$faiss$query_source
#> [1] "fitted_index"
#> 
#> 
#> attr(,"faissR_nn")$cuvs
#> NULL
#> 
#> attr(,"faissR_nn")$spatial_index
#> NULL
#> 
#> attr(,"faissR_nn")$auto_selection
#> NULL
#> 
#> attr(,"faissR_nn")$metric_transform
#> NULL
#> 
#> attr(,"faissR_nn")$distance_transform
#> NULL
#> 
#> attr(,"faissR_nn")$input_type
#> [1] "float32"
#> 
#> attr(,"faissR_nn")$input_layout
#> [1] "r_double_column_major_to_row_major_float32;fitted_index_query:r_double_column_major_to_row_major_float32"
#> 
#> attr(,"faissR_nn")$input_owns_data
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$float32_compatibility_conversion
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$distance_type
#> [1] "double"
#> 
#> attr(,"faissR_nn")$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$query_source
#> [1] "fitted_index"

Immediate prediction uses the same interface:

immediate <- knn(
    Xtrain = x,
    Ytrain = groups,
    Xtest = x[1:6, , drop = FALSE],
    k = 7,
    backend = "cpu",
    method = "exact",
    metric = "euclidean",
    n_threads = 2
)

immediate
#> [1] A A A A A A
#> attr(,"faissR_nn")
#> attr(,"faissR_nn")$k
#> [1] 7
#> 
#> attr(,"faissR_nn")$requested_backend
#> [1] cpu
#> 
#> attr(,"faissR_nn")$requested_method
#> [1] exact
#> 
#> attr(,"faissR_nn")$tuning
#> [1] auto
#> 
#> attr(,"faissR_nn")$target_recall
#> [1] 0.99
#> 
#> attr(,"faissR_nn")$cagra_implementation
#> [1] <NA>
#> 
#> attr(,"faissR_nn")$cagra_build_algo
#> [1] <NA>
#> 
#> attr(,"faissR_nn")$backend
#> [1] faiss_flat_l2
#> 
#> attr(,"faissR_nn")$resolved_backend
#> [1] faiss_flat_l2
#> 
#> attr(,"faissR_nn")$metric
#> [1] euclidean
#> 
#> attr(,"faissR_nn")$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation
#> attr(,"faissR_nn")$approximation$strategy
#> [1] faiss_IndexFlatL2
#> 
#> attr(,"faissR_nn")$approximation$backend
#> [1] faiss_flat_l2
#> 
#> attr(,"faissR_nn")$approximation$library
#> [1] faiss
#> 
#> attr(,"faissR_nn")$approximation$metric
#> [1] euclidean
#> 
#> attr(,"faissR_nn")$approximation$input_type
#> [1] float32
#> 
#> attr(,"faissR_nn")$approximation$fitted_index
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$index_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$index_type
#> [1] IndexFlatL2ExternalPtr
#> 
#> attr(,"faissR_nn")$approximation$tuning_source
#> [1] none
#> 
#> attr(,"faissR_nn")$approximation$tuning_source
#> [1] none
#> 
#> attr(,"faissR_nn")$approximation$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$approximation$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$approximation$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$approximation$query_source
#> [1] fitted_index
#> 
#> 
#> attr(,"faissR_nn")$faiss
#> attr(,"faissR_nn")$faiss$index_type
#> [1] IndexFlatL2ExternalPtr
#> 
#> attr(,"faissR_nn")$faiss$backend
#> [1] cpu
#> 
#> attr(,"faissR_nn")$faiss$library
#> [1] faiss
#> 
#> attr(,"faissR_nn")$faiss$metric
#> [1] euclidean
#> 
#> attr(,"faissR_nn")$faiss$exact
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_trained
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$index_training_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$build_train_call_count
#> [1] 0
#> 
#> attr(,"faissR_nn")$faiss$search_train_call_count
#> [1] 0
#> 
#> attr(,"faissR_nn")$faiss$vectors_reused
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$faiss$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$faiss$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$faiss$query_source
#> [1] fitted_index
#> 
#> 
#> attr(,"faissR_nn")$cuvs
#> NULL
#> 
#> attr(,"faissR_nn")$spatial_index
#> NULL
#> 
#> attr(,"faissR_nn")$auto_selection
#> NULL
#> 
#> attr(,"faissR_nn")$metric_transform
#> NULL
#> 
#> attr(,"faissR_nn")$distance_transform
#> NULL
#> 
#> attr(,"faissR_nn")$input_type
#> [1] float32
#> 
#> attr(,"faissR_nn")$input_layout
#> [1] r_double_column_major_to_row_major_float32;fitted_index_query:r_double_column_major_to_row_major_float32
#> 
#> attr(,"faissR_nn")$input_owns_data
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$float32_compatibility_conversion
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$distance_type
#> [1] double
#> 
#> attr(,"faissR_nn")$batch_query
#> [1] TRUE
#> 
#> attr(,"faissR_nn")$query_n
#> [1] 6
#> 
#> attr(,"faissR_nn")$query_call_count
#> [1] 1
#> 
#> attr(,"faissR_nn")$query_source
#> [1] fitted_index
#> 
#> Levels: A B C

Nearest-neighbour graphs

knn_graph() builds a weighted graph from a data matrix or from a precomputed nn() result. Passing a precomputed KNN object is useful when the same neighbours are reused by several downstream methods.

g <- knn_graph(
    x,
    k = 8,
    backend = "cpu",
    method = "exact",
    metric = "euclidean",
    weight = "snn",
    n_threads = 2
)

c(
    n_vertices = g$n_vertices,
    n_edges = g$n_edges,
    weight_type = g$weight_type
)
#>  n_vertices     n_edges weight_type 
#>        "90"      "1277"       "snn"
head(g$from)
#> [1] 1 1 1 1 1 1
head(g$to)
#> [1] 2 3 4 5 6 7

Graph clustering

graph_cluster() accepts a graph, a KNN object, or a matrix. CPU graph clustering is implemented in native C++ and does not depend on igraph. Available clustering methods are random_walking, louvain, and leiden.

clusters <- graph_cluster(
    g,
    method = "louvain",
    backend = "cpu",
    n_threads = 2,
    seed = 1
)

c(
    method = clusters$method,
    backend = clusters$backend,
    n_communities = clusters$n_communities,
    modularity = round(clusters$modularity, 3)
)
#>        method       backend n_communities    modularity 
#>     "louvain"         "cpu"           "4"       "0.631"
table(clusters$membership, groups)
#>    groups
#>      A  B  C
#>   1 30  0  0
#>   2  0  4  0
#>   3  0 26  0
#>   4  0  0 30

For Louvain and Leiden, n_clusters is an optional target-count convenience parameter. It is an alternative to direct resolution control and is not a hard guarantee.

targeted <- graph_cluster(
    g,
    method = "louvain",
    backend = "cpu",
    n_clusters = 3,
    n_threads = 2,
    seed = 1
)

c(
    requested = targeted$parameters$n_clusters_requested,
    observed = targeted$n_communities,
    selected_resolution = round(targeted$selected_resolution, 3)
)
#>           requested            observed selected_resolution 
#>                3.00                3.00                0.02

k-means

fast_kmeans() uses the same backend style as the nearest-neighbour functions. The CPU route is available everywhere; CUDA is optional.

km <- fast_kmeans(
    x,
    centers = 3,
    backend = "cpu",
    n_init = 2,
    max_iter = 20
)

c(
    backend = km$backend,
    backend_library = km$backend_library,
    converged = km$converged
)
#>         backend backend_library       converged 
#>         "faiss"         "faiss"         "FALSE"
table(km$cluster, groups)
#>    groups
#>      A  B  C
#>   1  0  0 30
#>   2 30  0  0
#>   3  0 30  0

Optional CUDA use

CUDA-specific calls should be guarded with cuda_available() or inspected with backend_info(). The example below is not evaluated on CPU-only builders.

if (cuda_available()) {
    gpu_knn <- nn_gpu(
        x,
        k = 15,
        exclude_self = TRUE,
        method = "auto",
        metric = "euclidean",
        tuning = "auto",
        target_recall = 0.99
    )

    gpu_knn
    host_copy <- gpu_knn_to_host(gpu_knn)
}

nn_gpu() is for downstream CUDA consumers. It keeps result buffers resident on the GPU and copies them back to R only when gpu_knn_to_host() is called explicitly.

Session information

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] faissR_0.99.15   BiocStyle_2.41.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.39       R6_2.6.1            fastmap_1.2.0      
#>  [4] xfun_0.60           maketools_1.3.2     float_0.3-3        
#>  [7] cachem_1.1.0        parallel_4.6.1      knitr_1.51         
#> [10] htmltools_0.5.9     rmarkdown_2.31      buildtools_1.0.0   
#> [13] lifecycle_1.0.5     cli_3.6.6           sass_0.4.10        
#> [16] jquerylib_0.1.4     compiler_4.6.1      sys_3.4.3          
#> [19] tools_4.6.1         bslib_0.11.0        evaluate_1.0.5     
#> [22] Rcpp_1.1.2          yaml_2.3.12         otel_0.2.0         
#> [25] BiocManager_1.30.27 jsonlite_2.0.0      rlang_1.3.0