KODAMA

August 12, 2026 ยท View on GitHub

R interface to the standalone kodama-cpp C++/CUDA/Metal library.

The wrapper and numerical core are maintained as separate repositories:

Quick Install

The source package contains the portable MIT-licensed CPU core and does not require a separately installed C++ library:

git clone https://github.com/tkcaccia/KODAMA.git
Rscript -e 'install.packages("Rcpp", repos = "https://cloud.r-project.org")'
R CMD INSTALL KODAMA

Then verify the installation with:

library(KODAMA)
KODAMA.diagnostics()
help(package = "KODAMA")

The sections below explain optional CUDA, Metal, and development workflows.

Set one default backend for KODAMA, fastPLS, fastEmbedR, and faissR in the current R session, or through the process environment:

options(backend = "cuda")
# or: Sys.setenv(BACKEND = "cuda")

An explicit function argument such as backend = "cpu" always overrides the shared setting.

KODAMA does not reimplement the mathematics in R. It converts R matrices and vectors to the C++ API and returns R-friendly lists, matrices, and S3 objects. The vendored CPU sources are synchronized from kodama-cpp; the same public core is shared by the independently maintained R and Python wrappers.

What Is Linked

The normal source installation compiles the Rcpp bridge and portable CPU core:

R session -> KODAMA R functions -> Rcpp bridge -> vendored kodama-cpp CPU core

The wrapper exports:

  • KNNCV() and PLSLDACV() for cross-validated classifier kernels.
  • CoreKNN() and CorePLSLDA() for label-optimization kernels.
  • KODAMA.matrix() for complete KODAMA matrix construction.
  • KODAMA.matrix.graph() for bare neighbor-index/distance input.
  • KODAMA.pca() / kodama_pca() for backend-native float32 PCA.
  • KODAMA.visualization() for UMAP/openTSNE embeddings from KODAMA graphs.
  • KODAMA.graph(), KODAMA.makeSNNGraph(), makeSNNGraph(), and KODAMA.clustering() for graph construction and CPU random-walk clustering.
  • RunKODAMAgraph(), RunKODAMAmatrix(), and RunKODAMAvisualization() for SingleCellExperiment, SpatialExperiment, Seurat, and Giotto containers.
  • SpatialFeatureSelection() / RunSpatialFeatureSelection() for SpatialExperiment, Seurat, and Giotto containers. The selector is multicore CPU-only.

Single-Cell Object Workflow

The three object generics mirror the matrix pipeline. Graph construction is performed once and retained as an opaque native handle; the matrix step reuses that graph, and only the visualization step creates a KODAMA reduced dimension:

object <- RunKODAMAgraph(
  object,
  reduction = "PCA", # use "pca" for Seurat and Giotto
  dims = 50,
  backend = "cpu"
)
object <- RunKODAMAmatrix(
  object,
  reduction = "PCA",
  dims = 50,
  classifier = "pls_lda",
  backend = "cpu",
  M = 100,
  Tcycle = 100
)
object <- RunKODAMAvisualization(
  object,
  reduction = "PCA",
  dims = 50,
  method = "UMAP",
  backend = "cpu"
)

For SpatialExperiment, spatial coordinates are used by default and colData(object)$sample_id keeps slides independent. Set use.spatial = FALSE for a data-only analysis or change sample.column when slide identifiers use another column. Seurat spatial methods obtain coordinates from the image objects and use image names as slide identifiers. Giotto methods use its selected spatial-location object. Lists of Seurat objects are processed element by element.

Bioconductor containers retain graph and matrix state under metadata(object)$KODAMA$KODAMA. Seurat stores the same state in Misc(object, "KODAMA")$KODAMA; Giotto stores it in the misc field of its KODAMA dimensional-reduction object. The final coordinates are available from reducedDim(object, "KODAMA"), Embeddings(object, "KODAMA"), or the corresponding Giotto dimensional reduction.

Prerequisites

The portable package requires R, Rcpp, and a C++17 compiler. CMake is needed only for optional accelerator builds against a separate kodama-cpp checkout.

On macOS with Homebrew, the CPU development environment is typically:

brew install cmake libomp
Rscript -e 'install.packages(c("Rcpp", "testthat"), repos = "https://cloud.r-project.org")'

On Linux/CUDA machines, use the same CUDA environment used to build the core. The important rule is that R CMD INSTALL and later R sessions must see the same CUDA Toolkit libraries used by kodama-cpp.

Optional accelerator installation

From a checkout where kodama-cpp and KODAMA are siblings:

cmake -S ../kodama-cpp -B ../kodama-cpp/build -DKODAMA_ENABLE_CUDA=OFF
cmake --build ../kodama-cpp/build -j

From this monorepo-style development checkout:

cmake -S ../.. -B ../../build -DKODAMA_ENABLE_CUDA=OFF
cmake --build ../../build -j

For a CUDA build, use a separate build directory:

cmake -S ../kodama-cpp -B ../kodama-cpp/build-cuda -DKODAMA_ENABLE_CUDA=ON
cmake --build ../kodama-cpp/build-cuda -j

For a native Metal build on macOS:

cmake -S ../kodama-cpp -B ../kodama-cpp/build-metal \
  -DKODAMA_ENABLE_CUDA=OFF \
  -DKODAMA_ENABLE_METAL=ON
cmake --build ../kodama-cpp/build-metal -j

The portable default does not use environment variables. To opt into a CUDA or Metal build, provide two paths:

  • KODAMA_CPP_ROOT: directory containing include/kodama/kodama.hpp.
  • KODAMA_CPP_BUILD_DIR: directory containing libkodama_cpp.a, libkodama_cpp.so, or libkodama_cpp.dylib.

CPU install from a sibling checkout:

cd KODAMA
KODAMA_CPP_ROOT="$(cd ../kodama-cpp && pwd)" \
KODAMA_CPP_BUILD_DIR="$(cd ../kodama-cpp/build && pwd)" \
R CMD INSTALL .

CPU install from this development checkout:

cd split-repos/KODAMA
KODAMA_CPP_ROOT="$(cd ../.. && pwd)" \
KODAMA_CPP_BUILD_DIR="$(cd ../../build && pwd)" \
R CMD INSTALL .

During configuration, the wrapper records the selected core-library path and checksums of the core archive and public headers. If any of them changes, both Rcpp bridge objects are rebuilt and the package is relinked automatically; an unchanged core retains normal incremental-install behavior. This prevents a development reinstall from silently loading a wrapper linked against an older CPU, CUDA, or Metal build. R CMD INSTALL --preclean remains useful for a fully clean diagnostic build, but is not required merely because the selected core was rebuilt or changed.

CUDA install uses the CUDA-enabled build directory and the runtime library environment. On a Linux machine with a conda/micromamba CUDA environment:

export ENV_DIR=/path/to/cuda-runtime-env
export CONDA_PREFIX="$ENV_DIR"
export LD_LIBRARY_PATH="$ENV_DIR/lib:$ENV_DIR/targets/x86_64-linux/lib:/usr/local/cuda/targets/x86_64-linux/lib:${LD_LIBRARY_PATH:-}"

cd KODAMA
KODAMA_CPP_ROOT="$(cd ../kodama-cpp && pwd)" \
KODAMA_CPP_BUILD_DIR="$(cd ../kodama-cpp/build-cuda && pwd)" \
R CMD INSTALL .

If extra CUDA libraries are needed by your local static link, provide them through KODAMA_R_CUDA_LIBS:

export KODAMA_R_CUDA_LIBS="-lcudart -lcublas -lcusolver -lcusparse"

The package configure script writes src/Makevars during installation. It uses only library directories that exist on the current machine, which keeps local checks quiet while still allowing CUDA/conda paths on GPU hosts. On macOS it links the Metal, Metal Performance Shaders, and Foundation frameworks.

Metal installation uses the Metal-enabled core build:

cd KODAMA
KODAMA_CPP_ROOT="$(cd ../kodama-cpp && pwd)" \
KODAMA_CPP_BUILD_DIR="$(cd ../kodama-cpp/build-metal && pwd)" \
R CMD INSTALL .

Verify The Installation

Start R and check the linked runtime:

library(KODAMA)
KODAMA.diagnostics()

Run a small CPU smoke test:

set.seed(1)
x <- matrix(rnorm(120 * 8), 120, 8)
lab <- rep(1:3, length.out = nrow(x))

cv <- KNNCV(x, lab, folds = 3, k = 5, backend = "cpu")
cv$accuracy

pc <- KODAMA.pca(x, ncomp = 3, backend = "cpu")
dim(pc$scores)

kk <- KODAMA.matrix(
  x,
  classifier = "knn",
  backend = "cpu",
  M = 2,
  Tcycle = 2,
  landmarks = 80,
  progress = FALSE
)

KODAMA.timing(kk)
kk$landmark_seconds
head(kk$best_labels)

progress_path <- tempfile(fileext = ".log")
kk_progress <- KODAMA.matrix(
  x, M = 2, Tcycle = 10, progress = TRUE,
  progress.file = progress_path
)
tail(readLines(kk_progress$progress_file))

prepared <- KODAMA.graph(
  x, k = 30, backend = "cpu", storage = "handle"
)
stopifnot(is.null(prepared$data))
kk_from_graph <- KODAMA.matrix(
  graph = prepared,
  M = 2,
  Tcycle = 2,
  progress = FALSE
)
pls_from_graph_and_x <- KODAMA.matrix(
  data = x,
  graph = prepared,
  classifier = "pls_lda",
  M = 2,
  Tcycle = 2,
  progress = FALSE
)

With storage = "handle", the prepared object owns one native float32 graph without creating R index and distance matrices. It can be passed directly to KODAMA.matrix(), KODAMA.visualization(), and KODAMA.clustering(); call KODAMA.graph.materialize(prepared) only when R matrices are required. storage = "handle" is the default; request storage = "matrix" explicitly when R arrays are required. Both forms carry backend-matched UMAP/openTSNE PCA starts and never retain the raw matrix. External handles are process-local; materialize the graph before saving it for use in another R session. Native progress is written to a file because independent M workers must not call the R console from background threads. With progress = TRUE, the wrapper announces the path and returns it as progress_file; checkpoints report M, Tcycle, accuracy, score, active classes, acceptance, and elapsed time. Multiple slides can be kept distinct during spatial graph construction with the original KODAMA samples contract:

prepared <- KODAMA.graph(x, spatial = xy, samples = slide_id)
kk <- KODAMA.matrix(
  data = x, graph = prepared, spatial = xy, samples = slide_id,
  classifier = "knn"
)

The first spatial coordinate is offset slide by slide before spatial neighbors and constraints are built. Spatial landmark strata and optimization constraint IDs are also keyed by slide, so no constraint group can cross a slide boundary. A one-level samples vector is a no-op.

Repeated population collection coordinates can be handled with spatial.mode = "population". This opt-in mode regularizes identical latitude/longitude groups independently inside every M run using the classic harmonic attraction/repulsion and within/between-location equalization. The former ancestry = TRUE spelling has been removed; use spatial.mode = "population". Always report a genetics-only call without spatial alongside the geographic analysis.

KODAMA.matrix(..., return.graph = FALSE) returns labels only and omits the otherwise-unused final graph-distance correction. KODAMA.matrix() reserves data for the raw matrix and graph for a prepared graph or bare indices/distances list. Either can be supplied alone, or both can be supplied together.

For CUDA verification, switch backend = "cuda" after confirming that the R session can load the same CUDA Toolkit libraries used by the C++ build. For native KNN, PLS-LDA, Core, matrix, graph, and PCA verification on macOS, install against the Metal build and use backend = "metal". For CUDA or Metal KODAMA.matrix(), set n.cores = 0 to let the core select independent M lanes from device capacity and backend-specific workspace memory. Positive values remain explicit. The returned scheduler fields record the selected lane count and estimated memory per lane.

Preprocessing and example manifolds

The wrapper includes the dependency-free preprocessing and synthetic-data utilities from KODAMA:

x_normalized <- normalization(x, method = "pqn")$newXtrain
x_scaled <- scaling(x_normalized, method = "autoscaling", backend = "cpu",
                    n.cores = 4)$newXtrain

dini <- dinisurface(1000)
helix <- helicoid(1000)
spiral_clusters <- spirals(c(100, 100, 100), c(0.1, 0.1, 0.1))
roll <- swissroll(1000)

normalization() supports pqn, sum, median, sqrt, and none. scaling() supports none, centering, autoscaling, rangescaling, and paretoscaling. Both are thin calls to the standalone float32 C++ core and accept backend = "cpu", "cuda", "metal", or "auto"; no R numerical implementation is used, and their signatures and returned fields match the KODAMA R package.

Spatially variable features can be ranked independently across slides without SPARK-X or a GPL runtime dependency:

svg <- spatial_feature_selection(
  expression, coordinates, samples = slide_id,
  n.cores = 4
)
head(svg$features)

# Containers provide their own expression values, coordinates, and slide IDs.
svg_spe <- SpatialFeatureSelection(
  spe, assay.type = "logcounts", sample.column = "sample_id", n.cores = 4
)
svg_seurat <- SpatialFeatureSelection(
  seurat_object, assay = "RNA", layer = "data", n.cores = 4
)
svg_giotto <- SpatialFeatureSelection(
  giotto_object, values = "normalized", sample.column = "slide_id",
  n.cores = 4
)

The fixed low-rank projection statistic uses a multicore CPU implementation. Heavy arithmetic is float32; probability tails and BH adjustment are double precision. CUDA and Metal are intentionally not exposed for this function because they did not improve end-to-end runtime on the validation workloads.

Run R CMD check

The CRAN-style source-tarball procedure is documented once in inst/INSTALL.md. It compiles the Rcpp bridge, links libkodama_cpp, loads the package, runs the testthat suite, and builds the manual.

library(KODAMA)

kk <- KODAMA.matrix(
  x,
  classifier = "knn",
  backend = "cuda",
  M = 100,
  Tcycle = 20,
  knn.k = 30
)

KODAMA.timing(kk)
labels <- kk$best_labels
um <- KODAMA.visualization(kk, "UMAP", k = 30, backend = "cuda")
clu <- KODAMA.clustering(um, n.iterations = 10, random.walk.steps = 4)

KODAMA.matrix() returns a kodama_matrix object. The raw C++ fields are still available (res, acc, knn, timing), and convenience fields include best_labels, best_run, class_counts, and parameters.

The native matrix call builds the full-data graph once before all M searches and stores it in knn. It also performs one backend-native float32 PCA by default and derives both UMAP and openTSNE starts from the same scores. KODAMA.visualization() reuses these artifacts without another graph or PCA calculation. The observable fields are graph_builds and timing$visual_init_seconds. Only knn is serialized; the former duplicate base_knn payload has been removed. knn_is_kodama_corrected and graph_storage_bytes expose graph state and retained native capacity. An accelerator call returns labels and scores without materializing resident graph matrices by default. Set return.graph = TRUE when the result will be passed to KODAMA.visualization() or the graph matrices are otherwise required. Explicit init still has precedence, followed by a stored start whose backend matches the requested CPU, CUDA, or Metal visualization backend, then a start computed from raw.data. For example, after changing backends use:

um_cpu <- KODAMA.visualization(
  kk,
  "UMAP",
  raw.data = x,
  k = 30,
  backend = "cpu"
)

If none is available, UMAP uses graph-spectral initialization and openTSNE uses deterministic random initialization. The result attributes initialization and initialization_backend make the chosen path observable.

Troubleshooting

If installation cannot find the C++ headers, set KODAMA_CPP_ROOT explicitly.

If installation cannot find libkodama_cpp, build kodama-cpp first and set KODAMA_CPP_BUILD_DIR to the CMake build directory.

If loading fails with missing OpenMP or CUDA symbols, start R from the same shell where LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, CONDA_PREFIX, and related runtime variables are set.

If the CUDA backend is unavailable, confirm that kodama-cpp was configured with -DKODAMA_ENABLE_CUDA=ON and that the R wrapper was installed against that CUDA build directory.

If the Metal backend is unavailable, confirm that kodama-cpp was configured with -DKODAMA_ENABLE_METAL=ON and that the wrapper was installed against the same Metal build. UMAP and FFT-grid openTSNE both expose native Metal paths.