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
27 changes: 27 additions & 0 deletions .changeset/integer-count-bin-edges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@platforma-open/milaboratories.feature-integration.per-cell-metrics': patch
'@platforma-open/milaboratories.feature-integration.ui': patch
---

The fitted background draws the distribution the run holds

Two defects, one picture. The fitted-background grid is the only way to see whether a tag's counts
separated into two populations, so a hump it invents is the one error this surface cannot carry.

**Bins now sit on whole numbers.** `np.geomspace` puts edges between whole numbers, and a UMI count is
a whole number, so a bin could fall strictly between two counts and stand empty at every weight a run
could produce. On a 15-tag run topping out at 5,155 counts the bin at [2.039, 2.911) held nothing on
all 15 panels and read as a missing bar. Edges are now whole and strictly increasing, so every bin
holds at least one count. The low end steps by 1. Above that the step is geometric as before. The last
edge is one past the top count, which makes every bin half-open. A run now takes at most 24 bins
instead of always 24.

**Bars are now density, cells per count.** Bin width in counts rises across the edge set — 1, 1, 2, 3,
4, 6, 9, 14, 21 on one real run — so a bin spanning 4 counts stood about four times a neighbour
spanning 1 at equal density. That step read as a second hump on tags whose counts only decay. Dividing
each weight by the counts its bin spans removes it. The y axis is labelled "Cells per count", and each
panel's caption carries the cell count the plot is drawn from.

The hover readout on that grid now reports the density rather than the cell count, because
`PlChartHistogram` prints the number it is handed under a fixed `count:` label. The caption carries the
magnitude instead.
14 changes: 14 additions & 0 deletions .changeset/run-quality-tabs-hidden-while-running.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@platforma-open/milaboratories.feature-integration.ui': patch
---

Run quality hides its tab strip while the run computes

The tab set is derived from the baseline rung the run reports. Until the run
settles that rung is unknown, so the strip offered every plot and then dropped
the ones the served rung cannot draw — a reader could open a tab that then
stopped existing.

The strip is now hidden while the block computes. The open view's body keeps
rendering and draws its own processing placeholder, so the page still shows the
run's progress.
29 changes: 29 additions & 0 deletions .changeset/undeclared-status-per-barcode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@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': minor
---

The undeclared-barcode Status judges each barcode, not the whole sample

The Status column read the sample's aggregate undeclared share, so it was one
word repeated down every row of a sample. A sample carrying one heavy
undeclared sequence among many light ones said nothing about which sequence to
look at.

Status now reads each row's own share of its sample's pre-refine reads. It warns
above 1% and alerts above 5%. Those two numbers are operator-set and
overridable, not inherited: the field publishes 0.50/1.0 for a sample's
AGGREGATE undeclared share, and that line does not transfer to one sequence,
because an aggregate reaches 0.50 while no single sequence comes near it.

The alert end changed direction with it. It compared for equality, which fired
only at exactly the error threshold and let every larger share read *warn* — the
worse finding being the one that never showed. Both ends now face the same way.

The sample-level share keeps its column, renamed to "Sample Undeclared (%)", and
carries no status.

Every column description in that table was rewritten to one instruction per
sentence, active voice, and short sentences.
47 changes: 42 additions & 5 deletions model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import type {
} from "@platforma-sdk/model";
import {
BlockModelV3,
createPlDataTableStateV2,
createPFrameForGraphs,
createPlDataTableStateV2,
createPlDataTableV2,
createPlDataTableV3,
DataColumn,
DataModelBuilder,
getAxisId,
isPColumnSpec,
parseResourceMap,
getAxisId,
} from "@platforma-sdk/model";
import { assemblePattern, CELL_TAG, FEATURE_TAG, UMI_TAG, validatePattern } from "./pattern";
import { getPreset } from "./presets";
Expand All @@ -41,6 +41,12 @@ export const VERDICT_DEFAULTS = {
countFloor: 4,
// verdict.py BOUND_CUTOFF
boundCutoff: 75,
// verdict.py DISTRIBUTION_BOUND_PROBABILITY. Both the default and the FLOOR: `args()` refuses below it.
boundProbability: 0.9,
// tag_distribution.py DEFAULT_INITIAL_SIGNAL_WEIGHT, the source paper's own initial weight. A default
// only -- unlike the line above this is not a floor, because it states what the EXPERIMENT holds
// rather than what the method licenses, and a sorted or synthetic population can sit far from it.
expectedBinderFraction: 0.1,
// combine.py DEFAULT_MIN_VOTERS
minVotingCells: 1,
// verdict.py DEFAULT_PANEL_MIN_MEMBERS. Gates rather than tunes: keep above the fifteen-tag cap of an
Expand All @@ -64,8 +70,8 @@ export const QC_LINE_DEFAULTS = {
readsPerCellWarn: 5000,
aggregateBarcodeWarn: 0.05,
aggregateBarcodeError: 1.0,
undeclaredBarcodeWarn: 0.5,
undeclaredBarcodeError: 1.0,
undeclaredBarcodeWarn: 0.01,
undeclaredBarcodeError: 0.05,
usableReadWarn: 0.2,
usableReadError: 0.0,
} as const;
Expand Down Expand Up @@ -215,6 +221,11 @@ export type TagCountBins = {
* label column reaches p-frame surfaces only. Absent in full for a run that finished before this key.
*/
tagLabels?: Record<string, string>;
/**
* The barcodes in the order the PANEL declares them, deduplicated on first appearance. A per-sample view
* reads in it, so that a barcode holds the same slot in every sample.
*/
tagOrder?: string[];
/**
* The fit's two means and the background's share of cells, at the same (sample, tag) grain as the bins.
* Here rather than in the p-frame beside them, so a grid of panels costs no driver query per panel.
Expand All @@ -224,7 +235,15 @@ export type TagCountBins = {
*/
fitsBySample: Record<
string,
Record<string, { backgroundMean: number; signalMean: number; backgroundWeight: number }>
Record<
string,
{
backgroundMean: number;
signalMean: number;
backgroundWeight: number;
boundAtCount?: number | null;
}
>
>;
/**
* The run's own two spreads, each on its own LINEAR edges: `score` and `referenceReading`. Linear, unlike
Expand Down Expand Up @@ -505,8 +524,10 @@ type BlockDataV2 = Omit<
| "distributionMinCells"
| "countFloor"
| "boundCutoff"
| "boundProbability"
| "minVotingCells"
| "minAgreement"
| "expectedBinderFraction"
| "gateThreshold"
| "grouping"
| "contendingGroups"
Expand Down Expand Up @@ -710,6 +731,13 @@ export const platforma = BlockModelV3.create(dataModel)
);

if (data.countFloor < 0) throw new Error("The count floor cannot be negative");
if (
typeof data.boundProbability === "number" &&
(data.boundProbability < VERDICT_DEFAULTS.boundProbability || data.boundProbability > 1)
)
throw new Error(
`The fitted baseline's probability is at least ${VERDICT_DEFAULTS.boundProbability} and at most 1`,
);
if (data.boundCutoff < 0 || data.boundCutoff > 100)
throw new Error("The bound cutoff is a score between 0 and 100");
if (data.minVotingCells < 1) throw new Error("At least one cell must vote");
Expand All @@ -722,6 +750,13 @@ export const platforma = BlockModelV3.create(dataModel)
(data.minAgreement <= 0.5 || data.minAgreement > 1)
)
throw new Error("The agreement floor is a share above 50% and at most 100%");
// Strictly inside (0, 1). At either end the split hands every cell to one side and the fit silently
// falls back, so the run would record a fraction it never used.
if (
typeof data.expectedBinderFraction === "number" &&
(data.expectedBinderFraction <= 0 || data.expectedBinderFraction >= 1)
)
throw new Error("The expected binder fraction is a share above 0% and below 100%");
// The cell condition GATES the fitted rung rather than tuning it, so it is a real population size.
if (data.distributionMinCells < 1)
throw new Error("A fitted baseline needs at least one cell to be fitted over");
Expand Down Expand Up @@ -855,6 +890,8 @@ export const platforma = BlockModelV3.create(dataModel)
distributionMinCells: Math.round(data.distributionMinCells),
countFloor: Math.round(data.countFloor),
boundCutoff: data.boundCutoff,
boundProbability: data.boundProbability,
expectedBinderFraction: data.expectedBinderFraction,
minVotingCells: Math.round(data.minVotingCells),
// Off by default, and off means ABSENT. A minimum agreement of 0 passes every majority instead of skipping
// the check. A gate of 0 sets aside every cell instead of gating none.
Expand Down
4 changes: 4 additions & 0 deletions model/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export type BlockArgs = {
distributionMinCells: number; // cells a sample needs before the rung may serve
countFloor: number; // counts below this are not evidence of binding
boundCutoff: number; // specificity score (0-100) at or above which a cell binds
boundProbability?: number; // the probability a count belongs to the signal component at or above which a cell binds
expectedBinderFraction?: number; // the share of cells expected to bind, seeding the fitted rung's split
minVotingCells: number; // a verdict may rest on one cell and say so
// Share (0-1) of answering cells the majority must reach. Off by default, and off means ABSENT rather
// than zero: a floor of 0 passes every majority instead of skipping the check.
Expand Down Expand Up @@ -188,6 +190,8 @@ export type BlockData = {
distributionMinCells: number;
countFloor: number;
boundCutoff: number;
boundProbability?: number;
expectedBinderFraction?: number;
minVotingCells: number;
minAgreement?: number;
gateThreshold?: number;
Expand Down
Loading
Loading