Skip to content

Commit 3eec1b8

Browse files
committed
spec R46: PlDatasetSelector via upstream pl7.app/isAnchor
The 3D Structure Prediction block published v1.0.11 with `pl7.app/isAnchor: "true"` on `pdbsMap` (platforma-open/3d-structure-prediction#13), unblocking this side. Model: - `BlockData.primaryRef: PrimaryRef` becomes `BlockData.dataset: DatasetSelection`. `.args()` unwraps `data.dataset.primary` back to the same `PrimaryRef` envelope the workflow already consumes (`wf.prepare` still reads `args.primaryRef.column` / `.filter`). - Replace the two separate outputs `pdbOptions` + `filterOptions` with a single `datasetOptions` driven by `buildDatasetOptions`. `primary` predicate matches `name == "pl7.app/structure/pdb"` with the new isAnchor annotation; `filter` predicate matches `predictionSuccessful` / `confident` by name. - `clonotypePdbsMap` and `clonotypeAxisId` resolve the picked PlRef through `ctx.data.dataset.primary.column` instead of the old `ctx.data.primaryRef.column`. UI: - Single `<PlDatasetSelector v-model="app.model.data.dataset">` in Settings replaces the two `PlDropdownRef` widgets (primary + filter). Drop the `primaryRefColumn` / `primaryRefFilter` read-write computeds, the `createPrimaryRef` helper import, and the `PlRef` type import. - `settingsOpen` / empty-state guard read `app.model.data.dataset?.primary?.column`. DataModel chain collapse (the block is unreleased, no published version is on disk anywhere): - Drop the entire 14-step migrate chain that accumulated during PR review. The persisted shape was never shipped so a chain that walks `v1` to `v14` has no real-world consumers. - Single `from<BlockData>("v1").init(...)` produces the current shape directly. - Drop the historical `BlockDataV1` to `BlockDataV13` type pyramid and the unused initialGraphState / createPlDataTableStateV2 / createPrimaryRef / GraphMakerState / ImportFileHandle / PlDataTableStateV2 / PlRef / PrimaryRef imports. Net: -221 LOC across model + ui + workflow. Block build 7/7; 65 pytest cases still pass. Verified end-to-end manually via pl MCP: dataset selector populates, picking a dataset propagates to workflow as PrimaryRef.column, optional filter dropdown attached to the same widget, Run completes against the upstream PDB column.
1 parent 194f404 commit 3eec1b8

3 files changed

Lines changed: 86 additions & 307 deletions

File tree

model/src/index.ts

Lines changed: 63 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -1,260 +1,67 @@
1-
import type { GraphMakerState } from "@milaboratories/graph-maker";
21
import type {
32
AxisId,
43
BlockRenderCtx,
5-
ImportFileHandle,
4+
DatasetOption,
5+
DatasetSelection,
66
InferOutputsType,
77
PColumn,
88
PColumnDataUniversal,
99
PColumnIdAndSpec,
1010
PFrameHandle,
11-
PlDataTableStateV2,
12-
PlRef,
13-
PrimaryRef,
11+
PObjectSpec,
1412
} from "@platforma-sdk/model";
1513
import {
1614
BlockModelV3,
17-
createPlDataTableStateV2,
15+
buildDatasetOptions,
1816
createPlDataTableV2,
19-
createPrimaryRef,
2017
DataModelBuilder,
2118
getAxisId,
19+
isPColumnSpec,
2220
parseResourceMap,
2321
} from "@platforma-sdk/model";
2422

25-
/** Spec R14 / R10 , numbering schemes supported for region tagging. */
23+
/** Spec R14 / R10: numbering schemes supported for region tagging. */
2624
export type NumberingScheme = "imgt" | "chothia" | "kabat";
2725

28-
/** Original shape; preserved so existing block instances migrate. */
29-
type BlockDataV1 = {
30-
pdb: ImportFileHandle | undefined;
31-
tableState: PlDataTableStateV2;
32-
};
33-
34-
/** v2: cysTableState was added alongside the motif tableState. */
35-
type BlockDataV2 = BlockDataV1 & {
36-
cysTableState: PlDataTableStateV2;
37-
};
38-
39-
/** v3: adds numbering scheme + heavy/light chain mapping so motif scoring
40-
* can apply R19 region weights and cysteines can be classified against
41-
* canonical positions (R21). All three are optional ("" / undefined means
42-
* "unknown" and motif scoring falls back to neutral weights). */
43-
type BlockDataV3 = BlockDataV2 & {
26+
export type BlockData = {
27+
/** Spec R1 / R46 dataset envelope picked via `PlDatasetSelector`.
28+
* `.args()` unwraps `dataset.primary` back into the `PrimaryRef`
29+
* shape the workflow's `wf.prepare` already knows how to resolve. */
30+
dataset?: DatasetSelection;
31+
/** R14 / R10 numbering scheme: empty string means unknown (motif
32+
* scoring then falls back to neutral region weights). */
4433
numberingScheme: NumberingScheme | "";
34+
/** R9 heavy / light chain overrides. Auto-detected from REMARK 99
35+
* PLATFORMA CDR records; these fields are populated only when the
36+
* user needs to override the detected mapping (e.g. on PDBs without
37+
* REMARK 99 records). */
4538
heavyChainId: string;
4639
lightChainId: string;
47-
};
48-
49-
/** v4 , adds R49 advanced thresholds (FR/CDR confidence gating + R12
50-
* buried/exposed rSASA cutoff). Defaults match the spec's calibrated
51-
* values for ImmuneBuilder. */
52-
type BlockDataV4 = BlockDataV3 & {
53-
rsasaBuriedCutoff: number;
40+
/** R34 region-aware confidence gating thresholds (Å). Calibrated for
41+
* ImmuneBuilder per-atom predicted error; raise for crystal PDBs
42+
* whose B-factor column carries Ų temperature factors. */
5443
frConfThresh: number;
5544
cdrConfThresh: number;
56-
};
57-
58-
/** v5 , adds `pdbRef`, a `PlRef` pointing at an upstream
59-
* `pl7.app/structure/pdb` PColumn (Spec R1-R6). When set, the workflow
60-
* resolves it into per-clonotype PDBs and runs the software once per
61-
* clonotype; output PColumns then key on `pl7.app/vdj/scClonotypeKey`
62-
* rather than the `structureId="static"` placeholder.
63-
*
64-
* `pdb` (the legacy `ImportFileHandle` upload) stays for the local
65-
* single-PDB dev path (e.g. 1N8Z). When both are set, `pdbRef` wins. */
66-
type BlockDataV5 = BlockDataV4 & {
67-
pdbRef?: PlRef;
68-
};
69-
70-
/** v6 , adds `scoresTableState` for the per-clonotype scalar metrics
71-
* table (Spec R51). Surfaces the existing `scoresData` PFrame the
72-
* workflow already emits. */
73-
type BlockDataV6 = BlockDataV5 & {
74-
scoresTableState: PlDataTableStateV2;
75-
};
76-
77-
/** v7 , adds GraphMaker state for the six Spec R54 distribution histograms:
78-
* PSH, PPC, PNC, SFvCSP (Fv), CDRH3 compactness (VHH), and the composite
79-
* developability score. */
80-
type BlockDataV7 = BlockDataV6 & {
81-
graphStatePshV2: GraphMakerState;
82-
graphStatePpcV2: GraphMakerState;
83-
graphStatePncV2: GraphMakerState;
84-
graphStateSfvcspV2: GraphMakerState;
85-
graphStateCdrh3CompactnessV2: GraphMakerState;
86-
graphStateDevScoreV2: GraphMakerState;
87-
};
88-
89-
/** v8 , adds R48 hydrophobicity scale selector. Removed in v13 when
90-
* the spec refresh fixed Hydrophobicity to KD (R48 removed from the
91-
* requirements list, spec Concept line: "Hydrophobicity is KD
92-
* min-max-normalized to [1.0, 2.0]"). */
93-
type BlockDataV8 = BlockDataV7 & {
94-
hydrophobicityScale: "kd" | "ww" | "hessa" | "em" | "bm";
95-
};
96-
97-
/** v9 , strips the legacy single-PDB path's `pdb` field, the three
98-
* persisted-but-unused PlAgDataTable v-model states, and the six
99-
* GraphMakerState fields left over from the GraphMaker→SVG histogram
100-
* migration. */
101-
type BlockDataV9 = Omit<
102-
BlockDataV8,
103-
| "pdb"
104-
| "tableState"
105-
| "cysTableState"
106-
| "scoresTableState"
107-
| "graphStatePshV2"
108-
| "graphStatePpcV2"
109-
| "graphStatePncV2"
110-
| "graphStateSfvcspV2"
111-
| "graphStateCdrh3CompactnessV2"
112-
| "graphStateDevScoreV2"
113-
>;
114-
115-
/** v10 , drops `rsasaBuriedCutoff` per spec R12 (Raybould 0.075 is
116-
* hardcoded in the workflow now). */
117-
type BlockDataV10 = Omit<BlockDataV9, "rsasaBuriedCutoff">;
118-
119-
/** v11 , replaces the bare `pdbRef: PlRef` field with a `primaryRef:
120-
* PrimaryRef` envelope per spec R1. */
121-
type BlockDataV11 = Omit<BlockDataV10, "pdbRef"> & {
122-
primaryRef?: PrimaryRef;
123-
};
124-
125-
/** v12 , adds the dataset-level `detectedMode` per the refreshed spec
126-
* (BlockData definition lines 222-231). Resolved by a UI watcher after
127-
* the first successful run from the per-clonotype `pl7.app/liabilities/mode`
128-
* column (uniform by R7); read by R51 (column selection), R54
129-
* (mode-specific histogram), R55 (subtitle prefix). */
130-
type BlockDataV12 = BlockDataV11 & {
45+
/** Spec BlockData.detectedMode: dataset-level TAP / TNP resolved by
46+
* the UI after the first successful run from the per-clonotype
47+
* `pl7.app/liabilities/mode` column (uniform by R7). Drives R51
48+
* mode-specific column visibility, R54 mode-specific histogram
49+
* dispatch, and the R55 subtitle prefix. */
13150
detectedMode?: "TAP" | "TNP";
13251
};
13352

134-
/** Current shape (v13) , drops `hydrophobicityScale` per the refreshed
135-
* spec. R48 (5-scale selector) is gone from the requirements; the spec
136-
* Concept locks Hydrophobicity to KD min-max-normalized to [1.0, 2.0]. */
137-
export type BlockData = Omit<BlockDataV12, "hydrophobicityScale">;
138-
139-
const initialGraphState = (title: string, fillColor: string): GraphMakerState => ({
140-
title,
141-
template: "bins",
142-
currentTab: null,
143-
layersSettings: { bins: { fillColor } },
144-
axesSettings: {
145-
axisY: { axisLabelsAngle: 0 as const, scale: "linear" },
146-
other: { binsCount: 30 },
147-
},
148-
});
149-
150-
const dataModel = new DataModelBuilder()
151-
.from<BlockDataV1>("v1")
152-
.migrate<BlockDataV2>("v2", (v1) => ({
153-
...v1,
154-
cysTableState: createPlDataTableStateV2(),
155-
}))
156-
.migrate<BlockDataV3>("v3", (v2) => ({
157-
...v2,
158-
numberingScheme: "",
159-
heavyChainId: "",
160-
lightChainId: "",
161-
}))
162-
.migrate<BlockDataV4>("v4", (v3) => ({
163-
...v3,
164-
rsasaBuriedCutoff: 0.075,
165-
frConfThresh: 4.0,
166-
cdrConfThresh: 6.0,
167-
}))
168-
.migrate<BlockDataV5>("v5", (v4) => ({
169-
...v4,
170-
pdbRef: undefined,
171-
}))
172-
.migrate<BlockDataV6>("v6", (v5) => ({
173-
...v5,
174-
scoresTableState: createPlDataTableStateV2(),
175-
}))
176-
.migrate<BlockDataV7>("v7", (v6) => ({
177-
...v6,
178-
graphStatePshV2: initialGraphState("PSH distribution", "#7da3d1"),
179-
graphStatePpcV2: initialGraphState("PPC distribution", "#e5a06f"),
180-
graphStatePncV2: initialGraphState("PNC distribution", "#82c79c"),
181-
graphStateSfvcspV2: initialGraphState("SFvCSP distribution (Fv)", "#bb86d6"),
182-
graphStateCdrh3CompactnessV2: initialGraphState(
183-
"CDRH3 compactness distribution (VHH)",
184-
"#d6b06b",
185-
),
186-
graphStateDevScoreV2: initialGraphState("Developability score distribution", "#cf6e83"),
187-
}))
188-
.migrate<BlockDataV8>("v8", (v7) => ({
189-
...v7,
190-
hydrophobicityScale: "kd",
191-
}))
192-
.migrate<BlockDataV9>("v9", (v8) => {
193-
// Strip the persisted-but-unused fields. Destructuring discards them
194-
// from the object literal returned to downstream code; the runtime
195-
// payload effectively shrinks for every block instance that lands
196-
// here. `_unused` names silence the no-unused-vars lint.
197-
const {
198-
pdb: _pdb,
199-
tableState: _t1,
200-
cysTableState: _t2,
201-
scoresTableState: _t3,
202-
graphStatePshV2: _g1,
203-
graphStatePpcV2: _g2,
204-
graphStatePncV2: _g3,
205-
graphStateSfvcspV2: _g4,
206-
graphStateCdrh3CompactnessV2: _g5,
207-
graphStateDevScoreV2: _g6,
208-
...rest
209-
} = v8;
210-
return rest;
211-
})
212-
.migrate<BlockDataV10>("v10", (v9) => {
213-
// Spec R12 , drop the user-tunable rSASA cutoff; the python defaults
214-
// to 0.075 (the canonical Raybould 2019 value) and the workflow no
215-
// longer overrides it.
216-
const { rsasaBuriedCutoff: _r, ...rest } = v9;
217-
return rest;
218-
})
219-
.migrate<BlockDataV11>("v11", (v10) => {
220-
// Spec R1 , wire shape moves from bare `pdbRef: PlRef` to the
221-
// `PrimaryRef` envelope `{column: PlRef, filter?: PlRef}`. Existing
222-
// installations carrying a PlRef in `pdbRef` get wrapped via
223-
// `createPrimaryRef`; the filter slot stays undefined until the
224-
// user picks one in Settings.
225-
const { pdbRef, ...rest } = v10;
226-
return {
227-
...rest,
228-
primaryRef: pdbRef ? createPrimaryRef(pdbRef) : undefined,
229-
};
230-
})
231-
.migrate<BlockDataV12>("v12", (v11) => ({
232-
// Spec BlockData definition (lines 222-231): dataset-level
233-
// detectedMode resolved by the UI after the first successful run.
234-
// Undefined until then, so the migration just adds the field as
235-
// undefined; the UI watcher fills it once scoresTable is ready.
236-
...v11,
237-
detectedMode: undefined,
238-
}))
239-
.migrate<BlockData>("v13", (v12) => {
240-
// R48 dropped from the refreshed spec; existing instances may carry
241-
// the legacy field. Strip it so the model shape matches the new spec.
242-
const { hydrophobicityScale: _drop, ...rest } = v12;
243-
return rest;
244-
})
245-
.init(() => ({
246-
primaryRef: undefined,
247-
// R14 default scheme , upstream's 3D Structure Prediction block always
248-
// produces IMGT-numbered PDBs (the `pl7.app/structure/numbering` domain
249-
// we match on already requires `imgt`), so defaulting here saves the
250-
// first-run user a dropdown click. Override if you're feeding the block
251-
// a custom non-IMGT PDB.
252-
numberingScheme: "imgt",
253-
heavyChainId: "",
254-
lightChainId: "",
255-
frConfThresh: 4.0,
256-
cdrConfThresh: 6.0,
257-
}));
53+
const dataModel = new DataModelBuilder().from<BlockData>("v1").init(() => ({
54+
dataset: undefined,
55+
// R14 default scheme: upstream's 3D Structure Prediction block always
56+
// produces IMGT-numbered PDBs (the `pl7.app/structure/numbering`
57+
// domain we match on already requires `imgt`), so defaulting here
58+
// saves the first-run user a dropdown click.
59+
numberingScheme: "imgt",
60+
heavyChainId: "",
61+
lightChainId: "",
62+
frConfThresh: 4.0,
63+
cdrConfThresh: 6.0,
64+
}));
25865

25966
// Helpers for the per-metric histogram output pairs below. Each pair
26067
// resolves a single `scoresData` PColumn by name; `pfFromScores` wraps
@@ -278,45 +85,40 @@ function specFromScores(ctx: ScoresCtx, name: string): PColumnIdAndSpec | undefi
27885

27986
export const platforma = BlockModelV3.create(dataModel)
28087
.args((data) => {
281-
// Spec R1 , primary input is a `PrimaryRef` envelope. The workflow's
282-
// `wf.prepare` resolves `primaryRef.column` into the upstream PDB
283-
// PColumn for single-shot iteration; the optional `filter` slot
284-
// narrows the clonotype set in Python before processing.
285-
if (!data.primaryRef?.column) {
88+
// Spec R1 / R46. UI carries `dataset: DatasetSelection`; we unwrap
89+
// back to the PrimaryRef envelope the workflow already knows how to
90+
// resolve (`wf.prepare` reads `args.primaryRef.column` for the PDB
91+
// PColumn, `.filter` for the optional clonotype subset).
92+
if (!data.dataset?.primary?.column) {
28693
throw new Error("Pick a predicted structures dataset");
28794
}
28895
return {
289-
primaryRef: data.primaryRef,
96+
primaryRef: data.dataset.primary,
29097
numberingScheme: data.numberingScheme,
29198
heavyChainId: data.heavyChainId,
29299
lightChainId: data.lightChainId,
293100
frConfThresh: data.frConfThresh,
294101
cdrConfThresh: data.cdrConfThresh,
295102
};
296103
})
297-
// Spec R1-R6 , surface `pl7.app/structure/pdb` PColumns from the result
298-
// pool so the UI can show a dropdown of predicted-structure datasets.
299-
// Matches what the 3D Structure Prediction block exports (`pdbsMap`
300-
// PFrame, IMGT-numbered PDBs keyed by clonotype).
301-
.output("pdbOptions", (ctx) =>
302-
ctx.resultPool.getOptions([
303-
{
304-
name: "pl7.app/structure/pdb",
305-
domain: { "pl7.app/structure/numbering": "imgt" },
306-
},
307-
]),
308-
)
309-
// Spec R1 `PrimaryRef.filter` , surface Int/Boolean PColumns anchored on
310-
// the same scClonotypeKey axis as the PDB column so the user can pick
311-
// a subset filter (e.g. upstream `pl7.app/structure/predictionSuccessful`
312-
// or `pl7.app/structure/confident`). The workflow stages whatever the
313-
// user picks as a TSV sidecar and Python drops clonotypes whose value
314-
// is falsy before iterating.
315-
.output("filterOptions", (ctx) =>
316-
ctx.resultPool.getOptions([
317-
{ name: "pl7.app/structure/predictionSuccessful" },
318-
{ name: "pl7.app/structure/confident" },
319-
]),
104+
// Spec R1 / R46. `buildDatasetOptions` surfaces anchor-marked PColumns
105+
// (`pl7.app/isAnchor: "true"`) from the result pool as datasets the UI
106+
// shows in `PlDatasetSelector`. We accept anchors whose name is
107+
// `pl7.app/structure/pdb` (the 3D Structure Prediction block emits this
108+
// since v1.0.11). Filter predicate identifies the Boolean/Int subset
109+
// columns the user can pick to narrow the clonotype set per R47
110+
// (predictionSuccessful, confident).
111+
.output("datasetOptions", (ctx): DatasetOption[] | undefined =>
112+
buildDatasetOptions(ctx, {
113+
primary: (spec: PObjectSpec): boolean =>
114+
isPColumnSpec(spec) &&
115+
spec.name === "pl7.app/structure/pdb" &&
116+
spec.annotations?.["pl7.app/isAnchor"] === "true",
117+
filter: (spec: PObjectSpec): boolean =>
118+
isPColumnSpec(spec) &&
119+
(spec.name === "pl7.app/structure/predictionSuccessful" ||
120+
spec.name === "pl7.app/structure/confident"),
121+
}),
320122
)
321123
// Spec R51 , per-clonotype scalar metrics table. PColumns come from the
322124
// `scoresData` PFrame (axes: [scClonotypeKey]); enriched with upstream
@@ -461,7 +263,7 @@ export const platforma = BlockModelV3.create(dataModel)
461263
// was returning empty here even with name + valueType + axes + domain
462264
// all aligned, so we use the canonical ref-based path , same as how
463265
// the workflow accesses it.
464-
const ref = ctx.args?.primaryRef?.column ?? ctx.data?.primaryRef?.column;
266+
const ref = ctx.args?.primaryRef?.column ?? ctx.data?.dataset?.primary?.column;
465267
if (!ref) return undefined;
466268
const pdbCol = ctx.resultPool.getPColumnByRef(ref);
467269
if (!pdbCol) return undefined;
@@ -479,7 +281,7 @@ export const platforma = BlockModelV3.create(dataModel)
479281
.output("clonotypeAxisId", (ctx): AxisId | undefined => {
480282
// Resolve through the user-picked PlRef inside the PrimaryRef envelope
481283
// (spec R1), then read its spec.
482-
const ref = ctx.args?.primaryRef?.column ?? ctx.data?.primaryRef?.column;
284+
const ref = ctx.args?.primaryRef?.column ?? ctx.data?.dataset?.primary?.column;
483285
if (!ref) return undefined;
484286
const pdbSpec = ctx.resultPool.getPColumnSpecByRef(ref);
485287
if (!pdbSpec) return undefined;

0 commit comments

Comments
 (0)