--- title: "faissR: nearest neighbours, graphs, and clustering" author: - name: "Stefano Cacciatore" package: faissR output: BiocStyle::html_document: toc: true toc_float: true vignette: > %\VignetteIndexEntry{faissR: nearest neighbours, graphs, and clustering} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4 ) ``` # 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`. ```{r data} 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) table(groups) ``` # 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. ```{r backends} backend_info()[, c("backend", "available", "knn_available")] ``` `nn_capabilities()` gives method, backend, and metric support. The table can be large, so the example below shows only CPU Euclidean rows. ```{r capabilities} caps <- nn_capabilities() subset( caps, backend == "cpu" & metric == "euclidean", select = c("method", "backend", "metric", "supported", "exact") ) ``` # Nearest-neighbour search The main entry point is `nn()`. Use `exclude_self = TRUE` for self-KNN when each row is searched against the same matrix and should not return itself. ```{r nn-exact} nearest <- nn( x, k = 5, backend = "cpu", method = "exact", metric = "euclidean", exclude_self = TRUE, n_threads = 2 ) nearest dim(nearest$indices) head(nearest$indices) head(round(nearest$distances, 3)) ``` The returned object stores both the public request and the implementation that actually ran. ```{r nn-metadata} c( requested_backend = attr(nearest, "requested_backend"), requested_method = attr(nearest, "requested_method"), backend_used = attr(nearest, "backend_used"), metric = attr(nearest, "metric"), distance_type = attr(nearest, "distance_type") ) ``` ## Automatic method selection The default `method = "auto"` uses compiled shape-, metric-, `k`-, backend-, and target-recall-aware rules. These rules are deterministic; they do not run a slow tuning pass inside ordinary user calls. ```{r nn-auto} auto_nearest <- nn( x, k = 8, backend = "cpu", method = "auto", metric = "euclidean", exclude_self = TRUE, n_threads = 2 ) c( backend_used = attr(auto_nearest, "backend_used"), requested_method = attr(auto_nearest, "requested_method") ) ``` For large two- or three-dimensional self-KNN, `method = "auto"` can select the native grid route. The explicit call below demonstrates that route on a small two-dimensional matrix. ```{r grid} x2 <- matrix(runif(2000), ncol = 2) grid_nearest <- nn( x2, k = 10, backend = "cpu", method = "grid", metric = "euclidean", exclude_self = TRUE, n_threads = 2 ) c( backend_used = attr(grid_nearest, "backend_used"), k = attr(grid_nearest, "k") ) ``` # 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. ```{r metrics} 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) ``` # 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"`. ```{r float32, eval=requireNamespace("float", quietly = TRUE)} 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") ) class(float_nearest$distances) ``` # 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. ```{r knn-fit} 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 round(prob, 3) ``` Immediate prediction uses the same interface: ```{r knn-immediate} immediate <- knn( Xtrain = x, Ytrain = groups, Xtest = x[1:6, , drop = FALSE], k = 7, backend = "cpu", method = "exact", metric = "euclidean", n_threads = 2 ) immediate ``` # 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. ```{r graph} 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 ) head(g$from) head(g$to) ``` # 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`. ```{r clustering} 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) ) table(clusters$membership, groups) ``` 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. ```{r target-clusters} 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) ) ``` # k-means `fast_kmeans()` uses the same backend style as the nearest-neighbour functions. The CPU route is available everywhere; CUDA is optional. ```{r kmeans} 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 ) table(km$cluster, groups) ``` # 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. ```{r cuda-example, eval=FALSE} 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 ```{r session-info} sessionInfo() ```