Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/deep-zebras-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@platforma-open/milaboratories.feature-integration.per-cell-metrics": minor
"@platforma-open/milaboratories.feature-integration.workflow": minor
"@platforma-open/milaboratories.feature-integration.model": minor
"@platforma-open/milaboratories.feature-integration.ui": minor
---

Visual refactoring
63 changes: 27 additions & 36 deletions docs/description.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
# Overview

This block reads feature-barcode sequencing and answers, for every clonotype in the run, what it bound.
The headline use case is antigen binding — BEAM- and LIBRA-seq–style experiments — but it works for any
barcoded feature, such as surface-protein tags.

It takes the feature-barcode FASTQ files, a panel file declaring what each barcode carries in each sample,
and the single-cell V(D)J dataset the clonotypes come from. It counts the barcodes per cell, reads each
count against a baseline you choose, and combines the cells of a clonotype into one answer per antigen.

**The answer has four states, not two.** A clonotype against an antigen reads *bound*, *not bound*, *never
asked* — no sample its cells came from offered that antigen, or the reagent returned nothing — or
*unreliable*, where the experiment did ask and the data cannot settle it. The two non-answers reach you
rather than being folded into *not bound*, because a clone shown to be clean of an off-target and a clone
never tested against it are different candidates, and the difference otherwise surfaces months later at the
cost of a made molecule.

Each verdict carries what it rests on: how many of the clonotype's cells could answer, how many read
bound, and — where you declared two antigens as competing for one site — the competitor that bound
instead.

**Which barcodes are read as one antigen is your choice, and it belongs to the question.** Group on the
antigen column and ten strains on twelve barcodes are ten answers; group on a family column and they are
one. Regrouping re-runs the reading alone, not the counting.

**What a count is read against is also your choice**, from the baselines the run supports: a control tag
declared in the panel and read in the same cell, or each barcode's own distribution across a sample's
cells. The block computes which are available and refuses to pick for you, because a baseline nobody chose
is a method nobody knows they used — and which one served travels with every verdict, since no two produce
comparable numbers.

The block also reports the quality of the run before you spend time on its biology — per sample, per
reagent, and per barcode the panel never declared — so a failed reagent or a mis-declared panel is visible
as itself rather than as a clone that did not bind.

The verdicts are keyed on the clonotype, so downstream blocks read them directly to filter and rank leads.

This block uses mitool, which is developed by MiLaboratories Inc. For more information, please see the
This block reads feature-barcode sequencing and determines, for every clonotype in the run, which antigens
it bound. The headline use case is antigen binding in BEAM- and LIBRA-seq-style experiments, but it works
for any barcoded feature, such as surface-protein tags. It links each antigen to the receptors that bound
it, producing results ready for lead selection.

The block takes the feature-barcode FASTQ files, a panel file declaring what each barcode carries in each
sample, and the single-cell V(D)J dataset the clonotypes come from. It uses mitool to parse the reads,
counts barcodes per cell, reads each count against a user-selected baseline — a control tag declared in the
panel, or each barcode's own distribution across a sample's cells — and combines the cells of a clonotype
into one verdict per antigen.

Every verdict is one of four states:

- **bound** — measured, and the cells cleared the defined bound threshold
- **not bound** — measured, and they did not
- **unreliable** — measured, but too few of its cells answered, or they disagreed
- **never asked** — no measurement exists: the antigen appeared in no read from any of those cells' samples

Each verdict also reports how many cells it rests on.

The block also reports run quality per sample and per reagent, along with any barcodes found in the reads
that the panel does not declare.

The verdicts are keyed on the clonotype, so downstream blocks read them directly — Labeling to turn
verdicts into named specificity labels, and Lead Selection to filter and rank candidates.

mitool is developed by MiLaboratories Inc. For more information, please see the
[mitool reference](https://mixcr.com/mixcr/reference/mitool-parse).
24 changes: 9 additions & 15 deletions model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1684,8 +1684,8 @@ export const platforma = BlockModelV3.create(dataModel)
value: "declared",
label: "Declared baseline tag",
description:
"The block reads each count against the tag your panel marks as the baseline, in the " +
"same cell. Verdicts read this way compare across runs.",
"Use this when your panel declares a control barcode. In each cell, an antigen counts as " +
"bound where its count clearly exceeds the control's.",
needs:
declaredTags.length > 0
? undefined
Expand All @@ -1698,12 +1698,9 @@ export const platforma = BlockModelV3.create(dataModel)
value: "distribution",
label: "Each tag's own distribution",
description:
`The block splits each tag's counts across a sample's cells into two components. It reads ` +
`each count against the lower component. The sample needs at least ` +
`${Math.round(ctx.data.distributionMinCells)} cells. Nothing checks that the two ` +
`components stand apart. The run shows each fit on the Run quality page, and you judge it. ` +
`Where no two-component fit can be computed, that tag gets no baseline, and only the ` +
`antigens it carries read unreliable. Select this where your panel declares no baseline tag.`,
`Use this when your panel has no control barcode. For each antigen, the block estimates ` +
`background from that antigen's own counts across a sample's cells, and a cell counts as ` +
`bound where its count is too high to be background.`,
},
];

Expand All @@ -1726,13 +1723,10 @@ export const platforma = BlockModelV3.create(dataModel)
{ type: "link" as const, href: "/" as const, label: "Main" },
...(hasRun
? [
{ type: "link" as const, href: "/qc" as const, label: "Per-sample QC" },
{ type: "link" as const, href: "/results" as const, label: "Per-cell results" },
// Shown for every run, including a run with no V(D)J dataset. That run produces no antigen columns, and
// this page is the only place a user learns why.
{ type: "link" as const, href: "/punchcard" as const, label: "Explore readout" },
// "Run quality" rather than "QC". That page is per SAMPLE and this one is per run.
{ type: "link" as const, href: "/antigen-qc" as const, label: "Run quality" },
{ type: "link" as const, href: "/qc" as const, label: "Sample QC" },
{ type: "link" as const, href: "/results" as const, label: "Cell counts" },
{ type: "link" as const, href: "/antigen-qc" as const, label: "Tag QC" },
{ type: "link" as const, href: "/punchcard" as const, label: "Clonotype binding" },
]
: []),
];
Expand Down
2 changes: 1 addition & 1 deletion model/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export type BlockData = {
defaultBlockLabel?: string; // UI-only: sidebar subtitle, mirrored from the suggestedBlockLabel output
tableState: PlDataTableStateV2; // per-cell results grid state (UI-only, never projected to args)
qcSummaryTableState: PlDataTableStateV2; // per-sample QC summary grid state (UI-only)
// The Run quality page's two grids (UI-only). Deliberately NOT named `antigenQcTableState` /
// The Per-tag QC page's two grids (UI-only). Deliberately NOT named `antigenQcTableState` /
// `panelMismatchTableState`: the v3 -> v4 migration strips those two keys. A stored grid state means
// something only against the frame it was saved on.
runQualityTableState: PlDataTableStateV2; // run-level quality measurements grid state
Expand Down
51 changes: 41 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ catalog:
"@milaboratories/ts-configs": 1.4.0
typescript: ~5.9.3
"@platforma-sdk/workflow-tengo": 6.8.3
"@platforma-sdk/block-tools": 2.14.3
"@platforma-sdk/block-tools": 2.14.4
"@platforma-sdk/model": 1.81.1
"@platforma-sdk/ui-vue": 1.81.1
"@platforma-sdk/test": 1.81.3
Expand Down
2 changes: 1 addition & 1 deletion software/per-cell-metrics/src/emit_verdicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ def main() -> None:
# cannot be caught from the settings: whether a sample holds three hundred cells whose counts
# admit a two-component fit is a property of the data. So the run FINISHES, says so, and draws
# no punchcard.
# The gate is not the comparator. `reference-two-roles` keeps a declared baseline tag's two
# The gate is not the comparator. A declared baseline tag has two
# roles apart: comparator always, admissibility gate only where a threshold is declared. Which
# rung supplies the comparator does not reach the gate, so the declared readings are built here
# too wherever the panel carries them. Without them a stored gate goes silently inert the moment
Expand Down
Loading
Loading