Coming from R spacexr#
rctd-py implements the same algorithm as R spacexr’s RCTD, with R’s parameter names and R’s defaults.
Agreement with R#
Measured against R spacexr 2.2.1 on 19 datasets (doublet mode, UMI_min=20), each side estimating
its own sigma, the noise parameter of the Poisson-lognormal model:
On the 11 datasets where rctd-py picks the same sigma as R, the calls match R on every pixel: 100% of spot classes and first types.
On the other 8, the two sigmas differ by 0.01-0.02, and 98.99-99.88% of spot classes agree.
Dataset |
Pixels |
Sigma (rctd-py / R) |
Spot class |
First type |
Dominant type |
|---|---|---|---|---|---|
Visium v1 FFPE Breast (55 µm spots) |
2,518 |
0.37 / 0.37 |
100% |
100% |
100% |
Visium v1 Breast (55 µm spots) |
3,798 |
0.30 / 0.30 |
100% |
100% |
100% |
Visium v2 CytAssist Breast (55 µm spots) |
4,992 |
0.41 / 0.42 |
99.64% |
99.88% |
99.94% |
Slide-seq Mouse Cerebellum |
11,626 |
0.88 / 0.88 |
100% |
100% |
100% |
Xenium Liver (small) |
13,936 |
0.62 / 0.62 |
100% |
100% |
100% |
Slide-seq Mouse Hippocampus |
17,747 |
0.52 / 0.52 |
100% |
100% |
100% |
StrataMap Breast (1.5 mm window) |
21,425 |
0.69 / 0.69 |
100% |
100% |
100% |
Xenium Mouse Brain |
36,362 |
0.69 / 0.69 |
100% |
100% |
100% |
VisiumHD Breast (segmented cells) |
38,990 |
0.67 / 0.68 |
99.47% |
99.77% |
99.71% |
Xenium Liver (large) |
58,187 |
0.72 / 0.70 |
99.05% |
99.42% |
99.73% |
VisiumHD Human (8 µm, K=78) |
102,971 |
0.47 / 0.47 |
100% |
100% |
>99.99% |
CosMx WTX Breast |
151,281 |
1.00 / 1.00 |
100% |
100% |
100% |
Xenium Breast |
162,023 |
0.62 / 0.63 |
99.46% |
99.72% |
99.86% |
Atera WTA Breast |
169,965 |
0.74 / 0.74 |
100% |
100% |
100% |
VisiumHD 3’ Mouse Brain (8 µm) |
371,461 |
0.40 / 0.41 |
99.85% |
99.88% |
99.89% |
VisiumHD Mouse Brain (8 µm) |
391,894 |
0.43 / 0.44 |
99.84% |
99.88% |
99.91% |
Xenium Prime 5K Breast |
503,411 |
0.65 / 0.64 |
99.88% |
99.86% |
99.91% |
VisiumHD Breast (8 µm bins) |
557,300 |
0.80 / 0.78 |
98.99% |
99.42% |
99.52% |
Atera WTA Cervical |
714,535 |
1.12 / 1.12 |
100% |
100% |
>99.99% |
The remaining difference is sigma alone. Both R and rctd-py estimate it from a random sample of pixels, and R does not fix its random seed, so two R runs on the same data can also pick sigmas 0.01 apart and then agree on only about 99.3% of spot classes (Xenium liver small). Given R’s sigma, rctd-py matches R on 100% of spot classes even on the dataset with the largest gap above (Visium HD breast, 557,300 bins).
Before 0.4.0, rctd-py agreed less on sequencing-based data (84.9% dominant-type agreement on Visium v1 breast) because its bulk fit used genes R leaves out; see the changelog.
Reproducing a given R run#
To match a specific R result, take its sigma and pass it to rctd-py, which then skips sigma estimation:
# R
sigma <- round(100 * myRCTD@internal_vars$sigma) # e.g. 63
result = run_rctd(spatial, reference, mode="doublet", sigma_override=63)
sigma_override is not needed for normal use; the sigma rctd-py estimates is as valid as R’s.
It is for validation against R, for migrating a project that already has R results, and for
keeping sigma fixed when you rerun on a subset of the data.
Neither package normalises full-mode weights: R’s myRCTD@results$weights and rctd-py’s
result.weights are both the raw fit. To compare them as proportions, normalise both per pixel
(in R, normalize_weights()). Doublet-mode weights_doublet and multi-mode sub_weights are
already normalised in both.
Parameter correspondence#
Reference#
R spacexr |
rctd-py |
Default |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
R stops with an error when a cell type has fewer than CELL_MIN_INSTANCE cells; rctd-py drops
the type and continues.
create.RCTD()#
These create.RCTD() arguments are fields of the same name and default in
RCTDConfig: gene_cutoff, fc_cutoff, gene_cutoff_reg, fc_cutoff_reg,
UMI_min, UMI_max, counts_MIN, UMI_min_sigma, MAX_MULTI_TYPES,
CONFIDENCE_THRESHOLD (5), DOUBLET_THRESHOLD (20) and class_df. CELL_MIN_INSTANCE is
cell_min on Reference. class_df is a dict {cell_type: class} in rctd-py
instead of R’s data frame with the cell types as row names and a class column.
R’s internal config entries are fields too: N_epoch, MIN_CHANGE_BULK, MIN_CHANGE_REG,
MIN_OBS and N_fit, with R’s defaults (MIN_CHANGE_REG is accepted but not read yet).
N_fit, the number of pixels sampled for sigma estimation, is 1000 as in R since 0.4.0 (100
before). The RCTDConfig reference lists what each field does.
max_cores has no equivalent: rctd-py vectorises across pixels instead of forking workers. On
CPU, set the thread count with OMP_NUM_THREADS. A GPU is used automatically when PyTorch sees
one (device="auto"); force a device with RCTDConfig(device="cuda") or --device cuda.
run.RCTD(doublet_mode = ...)#
R |
rctd-py |
|---|---|
|
|
|
|
|
|
|
|
|
|
Where R stops with an error, rctd-py carries on:
R’s multi mode stops the whole run when a pixel has no cell type above the weight threshold (in the benchmarks, after 3.2 hours on a 714,535-cell dataset); rctd-py keeps the type with the highest weight for that pixel.
R stops when fewer than 10 differentially expressed genes are selected; rctd-py prints a warning and uses all common genes (bulk fit) or the bulk gene list (pixel fits) instead.