1- import type { GraphMakerState } from "@milaboratories/graph-maker" ;
21import 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" ;
1513import {
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. */
2624export 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
27986export 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