Where the data and the method come from, then the settings and counts that the saved analysis records.
Data source. NeurIPS 2021 Open Problems BMMC CITE-seq (GEO GSE194122). Detailed dataset provenance and usage conditions are on the Dataset page.
Notebook lineage
- Author
- JAE — Qiao Liu & Wanwen Zeng, winners of the NeurIPS 2021 Joint Embedding task (team Amateur); reimplemented in PyTorch for this demo.
- Source / context
- PC5251 teaching demo: a walk through one joint-embedding method, then controlled experiments that change one part of its training at a time.
- Notebook
- jae_walkthrough.ipynb
What the saved analysis records
The data, settings and counts behind the panels on this page. The right-hand column names the analysis step that produced each value.
| Property | Recorded value | Produced by |
|---|---|---|
| Dataset | NeurIPS 2021 Open Problems BMMC CITE-seq · GEO GSE194122 | dataset record |
| Cells | 66,175 bone-marrow mononuclear cells | stage 1 · preprocessing |
| Measured per cell | 13,953 genes (GEX) and 134 surface proteins (ADT), read simultaneously | stage 1 · preprocessing |
| Labels kept out of the input | 45 cell types · 9 batches (site × donor) · cell-cycle phase | stage 1 · preprocessing |
| Cell-cycle scoring | log1p(.X) then zero-centre scale, matching upstream JAE · 43 S genes and 54 G2M genes used | stage 1 · preprocessing |
| Agreement with the dataset's own phase call | 31.32% of cells | stage 1 · preprocessing |
| Transformation | TruncatedSVD to 100 components on GEX (25.32% of variance); ADT kept at 134 and not reduced | stage 2 · transformation |
| Negative share of the model input | 21.47%, which the model's output layer (a ReLU, never negative) cannot reproduce | stage 2 · transformation |
| Latent layout | cell type 0–45 · batch 45–54 · cell cycle 54–56 · free 56–61 | stage 3 · modelling |
| Training | batch 256 · learning rate 0.001 · max 150 epochs, stop after 10 without improvement · 61 epochs run | stage 3 · training |
| Loss weights | 0.7 / 0.2 / 0.05 / 0.05 · batch term = uniform | stage 3 · training |
| Variants trained | 14 configurations, each with its own 2-D projection and its own six measures | stage 3 · training |
| Evaluation set | 66,175 cells; silhouette scores on a stratified subsample of 8,000 cells | stage 4 · evaluation |
| Held-out split | 6,618 cells (10%) chosen at random among cells (seed 42), not a held-out donor | stage 4 · evaluation |
| Analysis saved | 2026-07-30 17:57:45 | stage 4 · saving |
Counts in this table are for the full dataset. The matrix picture in the Design matrix section draws on a display sample of 5,004 cells × 160 columns.
All embeddings, metrics and training curves on this page come from the saved analysis.
How the six metrics are computed here, compared with the scib benchmarking package
- NMI
- scib tunes the resolution of Louvain clustering to maximise NMI (scib.cl.opt_louvain). Louvain is not available here, so KMeans is run over a range of k and the best NMI is kept. Both procedures tune on the metric itself; KMeans assumes compact, convex clusters and Louvain does not.
- cell-type ASW
- Same formula as scib, (ASW + 1)/2, but computed on a stratified subsample rather than on all cells, because silhouette scoring grows with the square of the cell count.
- batch ASW
- Same per-cell-type 1 − |silhouette| construction as scib, on the same subsample. Groups with fewer than 10 cells or a single batch are skipped, as in scib.
- graph connectivity
- Same definition as scib (for each cell type, the share of its cells in the largest connected part of a nearest-neighbour graph), with the graph built on the subsample.
- cell-cycle conservation
- scib compares, batch by batch, how much variance the S and G2M scores explain before and after integration. Here this is done with PCA and linear regression, and the “before” reference is the 234-value model input rather than scib's unintegrated data.
- trajectory conservation
- scib uses diffusion pseudotime (scanpy). Here it is replaced by the graph distance from the root cell on a nearest-neighbour graph, compared with the dataset's reference pseudotime by Spearman correlation.
What a row and a column are here
Cells × measured features (genes + proteins)
One row is one cell. The columns form two blocks, one per kind of measurement (genes, proteins), processed separately until Stage 2 joins them.
How a row is built
- One row is one bone-marrow cell. Its genes and proteins were measured in the same droplet, which is what lets one embedding combine them.
- The columns are two blocks side by side: 13,953 gene columns, then 134 protein columns. Both are normalised the same way; only the gene block is later reduced.
- An entry is the normalised abundance of that molecule in that cell — not a count, and not a probability.
- The 45 cell types, the 9 batches and the cell-cycle phase are labels y. They are never columns of X, and they reach the model only through the Stage 3 loss.
The picture below shows a sample of the matrix. It draws from 5,004 cells sampled across all cell types and 160 columns (26 marker genes and all 134 proteins); the counts above are for the full dataset.
One sample = one bone-marrow cell, measured two ways at once
CITE-seq reads two signals from the same cell: the mRNA it is currently transcribing (GEX, 13,953 genes) and the proteins sitting on its surface (ADT, 134 proteins, each detected by an antibody). Both describe the same cell, and a joint embedding combines them into one description.
Input X
- one sample
- one bone-marrow cell
- GEX
- 13,953 gene counts
- ADT
- 134 protein counts
- shape of X
- 66,175 × 14,087
Labels y (not inputs)
- cell type
- 45 types, annotated by hand
- batch
- 9 site × donor
- cell cycle
- continuous S / G2M scores
- pseudotime
- erythroid only, 10,373 cells
Task type
The goal is a useful description of each cell, not a prediction → unsupervised representation learning.
But JAE is semi-supervised: the labels never enter the input, only the loss function (the score training minimises).
The design-matrix bridge: what a row, a column and a single entry each are
Before any transformation, get the shape of X clear. In the figure below, every row = one cell, every column = one gene or one protein, and the intersection xij = the normalised abundance of that molecule in that cell. Click the heatmap to pick a cell (row) and a column, or use the menus and the Random cell button; the X[i,:] and X[:,j] panels follow.
X: a display slice of 66,175 × 14,087
X[i,:] → one cell, in its native units
X[:,j] → one column across all cells
The two modalities take different routes, then get concatenated
The gene block has nearly fourteen thousand columns and is extremely sparse (most entries are zero), so TruncatedSVD reduces it to 100 dimensions: 100 combined columns that keep its strongest patterns. The protein block has only 134 columns, each one a known protein, so it is left alone. Whether to reduce is decided for each kind of measurement separately.
GEX counts (66,175 × 13,953) │ L1 normalise ×1e4 → log10(1+x) └─ TruncatedSVD(100) ───────────┐ ├─→ concat → Xmodel (234 dims) → JAE ADT counts (66,175 × 134) │ │ L1 normalise ×1e4 → log10(1+x) │ └─ no reduction, keep 134 ──────┘ y (cell type / batch / cycle) bypasses this whole path. It appears only in the Stage 3 loss function.
How much variance each of the 100 components explains
The 61 numbers are split into blocks, each with its own job
JAE is an autoencoder: it compresses each cell's 234 input values into 61 numbers, the latent vector, and learns to rebuild the input from them. Each stretch of those 61 numbers has a job. JAE's cell-type "classifier" is not a separate network: it simply reads a slice of the latent vector (2_ae_pretrain_cite.py:272-274 in the upstream code). The chart shows the selected cell's 61 numbers:
Four losses, with the weights from the upstream code
| Loss | Latent block it acts on | Weight |
|---|---|---|
| MSE(decoder(z), x) | all 61 dims | 0.70 |
| CE(z[0:45], cell type) | first 45 dims | 0.20 |
| CE(z[45:54], uniform) | middle 9 dims | 0.05 |
| MSE(z[54:56], S/G2M scores) | next 2 dims | 0.05 |
Training: cell-type accuracy rises, batch accuracy falls to chance
Scoring the embedding, then tracing a point back to a real cell
Six metrics for the full JAE embedding
| Metric | All cells | Held-out only | Rewards |
|---|---|---|---|
| NMI agreement between clusters and cell type | 0.7775 | 0.7559 | biology kept |
| cell type ASW are cell types well separated | 0.6407 | 0.6240 | biology kept |
| cell cycle cons. did the cell-cycle signal survive | 0.8778 | 0.7897 | biology kept |
| trajectory cons. did the erythroid trajectory survive | 0.9011 | 0.9380 | biology kept |
| batch ASW are batches mixed within a cell type | 0.8379 | 0.8254 | batch removed |
| graph conn. does each cell type stay in one piece | 0.9316 | 0.9344 | batch removed |
| Mean of the six | 0.8278 | 0.8113 |
Biological conservation ↔ batch removal is a trade-off
The embedding: 61 numbers drawn as a 2-D map (UMAP)
Traceability: which cell is this point?
- barcode
- AGTTCCCTCTTCCTAA-1-s1d1
- true type
- pDC
- lineage
- Myeloid
- batch
- s1d1
- S / G2M
- -0.54 / -0.74
- erythroid pseudotime
- not applicable
- strongest genes
- MALAT1, MT-CO1, B2M, MT-CO3, MT-CO2
- training label
- held out (never trained on)
Lab: what each part of training changes
Six controlled experiments, each with one factor changed at a time: the batch-loss weight, the label losses, the batch target, the input measurements, the output layers and the random seed. Every variant shares the same data, the same train/validation split, the same architecture and the same random seed; only the factor named in each experiment differs. All 14 variants were trained in advance.
Each experiment shows its variants' 2-D maps of the same 5,004 cells, drawn on shared axes, then their metrics. Use “Colour every embedding below by” to recolour all maps; hover a point to identify the cell.
Raising the batch-loss weight from 0 to 0.5: is biological structure removed too?
The only thing that changes is λbatch, the weight on the batch loss (the third line of the loss table). The paper uses 0.05. Start with the maps coloured by batch: if batch correction is working, the colours within each group of cells should be thoroughly mixed.
λ_batch = 0
λ_batch = 0.05
λ_batch = 0.2
λ_batch = 0.5
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| λ_batch = 0 jae_batch0 | 0.792 | 0.641 | 0.891 | 0.928 | 0.850 | 0.944 | 0.813 | 0.897 | 0.841 | 84.8% |
| λ_batch = 0.05 jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
| λ_batch = 0.2 jae_batch20 | 0.785 | 0.643 | 0.873 | 0.757 | 0.821 | 0.945 | 0.765 | 0.883 | 0.804 | 84.9% |
| λ_batch = 0.5 jae_batch50 | 0.786 | 0.640 | 0.886 | 0.930 | 0.845 | 0.954 | 0.811 | 0.899 | 0.840 | 86.9% |
Which loss is the latent structure actually coming from?
Starting from "no training at all", add the supervision back one term at a time. These four variants answer the question: can an autoencoder grow cell-type structure on its own, or does it need labels to push it there?
① No training at all
② Plain autoencoder
③ + cell type
④ Full JAE
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| ① No training at all svd61 | 0.675 | 0.544 | 0.780 | 0.897 | 0.874 | 0.915 | 0.724 | 0.894 | 0.781 | — |
| ② Plain autoencoder ae_only | 0.511 | 0.486 | 0.671 | 0.742 | 0.855 | 0.813 | 0.603 | 0.834 | 0.680 | 1.1% |
| ③ + cell type ae_celltype | 0.779 | 0.648 | 0.865 | 0.919 | 0.838 | 0.930 | 0.803 | 0.884 | 0.830 | 84.3% |
| ④ Full JAE jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
The surprise is the NMI column (agreement between clusters and cell types): 0.675 → 0.511 → 0.779 → 0.777. The plain autoencoder (0.511) is worse than not training at all (0.675). A 61-dimensional bottleneck optimised purely for reconstruction does not organise cell-type structure better — it actually scrambles structure the SVD already had (remember the output ReLU truncates half the directions; see experiment 5). Only once type supervision is added does NMI jump to 0.779.
So the conclusion here is not "labels are the icing on the cake". It is: in this architecture, the readable cell-type structure in the latent space comes mainly from the labels, not from the reconstruction loss.
The key control: swap "uniform distribution" for "classify correctly"
This is the most important experiment on the page. JAE's batch term pushes the 9 batch logits toward a uniform distribution. The natural question is: what if it were an ordinary "get the batch right" term instead? Intuitively, getting it right means batch information is still sitting in the latent space — exactly what we did not want. The two variants are identical apart from that one line of the loss.
Push toward uniform (what JAE does)
Classify the batch correctly (control)
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| Push toward uniform (what JAE does) jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
| Classify the batch correctly (control) jae_batch_supervised | 0.608 | 0.524 | 0.919 | 0.704 | 0.666 | 0.806 | 0.689 | 0.736 | 0.704 | 86.0% |
What does the joint embedding actually buy over a single modality?
Zero out either the 100 gene (GEX) dimensions or the 134 protein (ADT) dimensions. Architecture, parameter count and training configuration all stay exactly the same; the model simply cannot see one of the two measurements. This measures what combining the two adds.
GEX only
ADT only
GEX + ADT jointly
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| GEX only jae_gex_only | 0.761 | 0.645 | 0.840 | 0.937 | 0.791 | 0.855 | 0.796 | 0.823 | 0.805 | 81.2% |
| ADT only jae_adt_only | 0.697 | 0.555 | 0.867 | 0.680 | 0.851 | 0.884 | 0.700 | 0.867 | 0.755 | 80.0% |
| GEX + ADT jointly jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
What do those two ReLUs cost?
Upstream puts a ReLU on the last layer of both the encoder and the decoder. The consequences: the decoder output is always ≥ 0, but 21% of the model input is negative (SVD components are signed), so that part is structurally unreconstructable; and the two cell-cycle dimensions are also pinned at ≥ 0, while about 70% of the true S/G2M scores are negative. jae_linear_out removes both ReLUs and changes nothing else.
As upstream (ReLU output)
Both ReLUs removed
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| As upstream (ReLU output) jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
| Both ReLUs removed jae_linear_out | 0.808 | 0.617 | 0.686 | 0.680 | 0.838 | 0.950 | 0.698 | 0.894 | 0.763 | 91.2% |
How much does the score move if you only change the random seed?
This measures how much the metrics move when nothing changes but the random seed: the yardstick for the differences in Experiments 1–5. The three variants differ only in torch.manual_seed.
Seed 0
Seed 1
Seed 2
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| Seed 0 jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
| Seed 1 jae_full_seed1 | 0.779 | 0.643 | 0.818 | 0.938 | 0.839 | 0.909 | 0.795 | 0.874 | 0.821 | 84.9% |
| Seed 2 jae_full_seed2 | 0.758 | 0.653 | 0.874 | 0.855 | 0.810 | 0.953 | 0.785 | 0.882 | 0.817 | 77.7% |
14 variants × six metrics
| Variant | NMI | type ASW | cycle cons. | traj. cons. | batch ASW | graph conn. | bio mean | batch mean | mean of 6 | cell-type acc. |
|---|---|---|---|---|---|---|---|---|---|---|
| No training · SVD to 61 dims svd61 | 0.675 | 0.544 | 0.780 | 0.897 | 0.874 | 0.915 | 0.724 | 0.894 | 0.781 | — |
| Plain autoencoder ae_only | 0.511 | 0.486 | 0.671 | 0.742 | 0.855 | 0.813 | 0.603 | 0.834 | 0.680 | 1.1% |
| AE + cell type ae_celltype | 0.779 | 0.648 | 0.865 | 0.919 | 0.838 | 0.930 | 0.803 | 0.884 | 0.830 | 84.3% |
| JAE λ_batch=0 jae_batch0 | 0.792 | 0.641 | 0.891 | 0.928 | 0.850 | 0.944 | 0.813 | 0.897 | 0.841 | 84.8% |
| Full JAE (paper config) jae_full | 0.777 | 0.641 | 0.878 | 0.901 | 0.838 | 0.932 | 0.799 | 0.885 | 0.828 | 86.5% |
| JAE λ_batch=0.2 jae_batch20 | 0.785 | 0.643 | 0.873 | 0.757 | 0.821 | 0.945 | 0.765 | 0.883 | 0.804 | 84.9% |
| JAE λ_batch=0.5 jae_batch50 | 0.786 | 0.640 | 0.886 | 0.930 | 0.845 | 0.954 | 0.811 | 0.899 | 0.840 | 86.9% |
| Batch term classifies correctly jae_batch_supervised | 0.608 | 0.524 | 0.919 | 0.704 | 0.666 | 0.806 | 0.689 | 0.736 | 0.704 | 86.0% |
| GEX only jae_gex_only | 0.761 | 0.645 | 0.840 | 0.937 | 0.791 | 0.855 | 0.796 | 0.823 | 0.805 | 81.2% |
| ADT only jae_adt_only | 0.697 | 0.555 | 0.867 | 0.680 | 0.851 | 0.884 | 0.700 | 0.867 | 0.755 | 80.0% |
| Output ReLUs removed jae_linear_out | 0.808 | 0.617 | 0.686 | 0.680 | 0.838 | 0.950 | 0.698 | 0.894 | 0.763 | 91.2% |
| Full JAE · seed 1 jae_full_seed1 | 0.779 | 0.643 | 0.818 | 0.938 | 0.839 | 0.909 | 0.795 | 0.874 | 0.821 | 84.9% |
| Full JAE · seed 2 jae_full_seed2 | 0.758 | 0.653 | 0.874 | 0.855 | 0.810 | 0.953 | 0.785 | 0.882 | 0.817 | 77.7% |
| Upstream hyperparameters, batch=32 lr=1e-4 jae_full_faithful | 0.769 | 0.624 | 0.657 | 0.927 | 0.846 | 0.951 | 0.744 | 0.898 | 0.796 | 82.9% |
The one exception is jae_full_faithful, which uses the upstream batch size and learning rate (batch 32, learning rate 1e-4) and so changes two factors at once; it is not part of any experiment above.
What the analysis shows
Each finding is followed by the numbers it rests on.
The joint embedding keeps cell-type structure while mixing batches
Four of the six metrics reward keeping biological structure and two reward removing batch effects. The full JAE model (jae_full) scores:
This is one point on the trade-off between the two, not a maximum of both.
Keeping biology and removing batch effects pull against each other
The trade-off chart in the Evaluation section plots every trained variant by the mean of its four biological metrics against the mean of its two batch metrics.
Experiment 1 in the lab moves along that trade-off by changing the batch-loss weight λbatch.
The batch loss works by flattening, not by predicting
The loss asks the batch block for equal probability over every batch, rather than for the right batch. Experiment 3 in the lab changes exactly that one line.
It is a simpler alternative to adversarial batch correction: no second network is trained to detect the batch. The loss constrains only those 9 numbers; the Insight page tests how often a cell's nearest neighbours, over all 61 numbers, share its batch.
Changing only the random seed moves the mean of six by about 0.01
Experiment 6 trains the same configuration three times, changing only the random seed. That spread is the bar any difference on this page has to clear.
Differences smaller than this are within seed-to-seed variation.
How to reproduce the analysis
- data
- GEO GSE194122 / openproblems S3
- SVD seed
- 123, 100 components
- train / val split
- 90/10, seed 42
- display sample
- 5,004 cells, stratified, seed 7
- cycle genes matched
- 43 S + 54 G2M / 97
- analysis saved
- 2026-07-30 17:57:45
python3 tools/precompute_features.py # normalise + SVD + cycle scores python3 tools/train_jae.py # all variants python3 tools/precompute_metrics.py # the six metrics python3 tools/precompute_page.py # pack the JSON payload python3 tools/build.py # inject into templates -> index.html
Glossary
Every term this page uses in a specific sense, with what it means here — on this dataset, in this experiment — rather than in general.
- CITE-seq
- a protocol that reads two signals from the same cell at once: the mRNA it is transcribing, and the proteins on its surface. That simultaneity is the entire reason a "joint" embedding is possible.
- GEX and ADT
- as used here: GEX is the gene-expression half of each cell's measurement (thousands of genes, very sparse); ADT is the antibody-derived-tag half (134 surface proteins, dense and individually interpretable). They take different routes through Stage 2 for exactly that reason.
- joint embedding
- as used here: one list of numbers per cell (here 61) that combines both measurements, rather than one list per measurement. The task is to produce a representation, not a prediction.
- the design matrix X, and the labels y
- as used here: one row of X is one cell; one column is one gene or one protein. Cell type, batch and cell-cycle phase are labels, not columns — they never enter the input and reach the model only through the loss function in Stage 3.
- TruncatedSVD
- a linear dimensionality reduction that does not centre the data first, which is what makes it usable on a sparse matrix. Here it takes GEX from thousands of genes down to 100 components; ADT is left alone.
- transductive
- as used here: the SVD is fitted on all the cells, including the ones later used for evaluation. Information from the evaluation cells therefore helps build the transformation. The competition permitted it.
- autoencoder, and the latent vector
- a network trained to reproduce its own input through a narrow middle layer. That middle layer's output — here 61 numbers per cell — is the embedding everything else on this page is computed from.
- the latent layout
- as used here: JAE's "classifier" is not a network, it is a slicing of those 61 numbers. Named ranges own named jobs — a cell-type block, a batch block, a cell-cycle pair, and a free remainder. The Transformation and modelling section draws the layout for the selected cell.
- the uniform batch loss
- as used here: the loss does not ask the model to get the batch wrong — that would still mean batch identity had been learned, merely inverted. It asks for equal probability over every batch, which removes batch information from those 9 numbers. It is a simpler alternative to adversarial batch correction: no second network is trained to detect the batch.
- λbatch
- the weight on that batch term. Experiment 1 in the lab tries 0, 0.05, 0.2 and 0.5; the upstream method uses 0.05.
- NMI
- normalised mutual information: how well a clustering of the embedding agrees with the annotated cell types, from 0 (no agreement) to 1 (perfect agreement). Here the clustering is KMeans, run over a range of k, keeping the best NMI.
- ASW (average silhouette width)
- as used here, two different things under one name: cell-type ASW rewards cells of one type sitting together, and batch ASW rewards batches being mixed within a cell type. One wants structure, the other wants structure destroyed — which is why they pull against each other.
- graph connectivity
- whether the cells of one type form a single connected neighbourhood rather than several islands. It penalises an embedding that shatters a cell type across the picture.
- cell-cycle conservation
- how much of the cell-cycle signal that was present before the embedding survives it. It is a conservation score: higher means less of that biology was scrubbed away.
- trajectory conservation
- whether an ordering that exists in the original data — here the erythroid maturation pseudotime — is still recoverable from the embedding.
- the six metrics, taken together
- four of them score biological conservation and two score batch removal. The trade-off chart in the Evaluation section plots the mean of the first four against the mean of the last two: top right is best, and no variant on this page reaches it.
- held-out 10%
- as used here: a random 10% of cells, at a recorded split seed — not a held-out donor. Every batch appears on both sides of that split.