Skip to content

Commit cfcfc72

Browse files
committed
spec R1: apply PrimaryRef.filter clonotype subset
Spec R1 wording: "optional `PrimaryRef.filter` reduces the clonotype set." Filter slot has been on the envelope since the chunk-1 work but the workflow ignored it. Wire it through end-to-end: Model: add `filterOptions` output surfacing compatible Boolean/Int PColumns the upstream prediction block emits (`pl7.app/structure/predictionSuccessful`, `pl7.app/structure/confident`). UI: split `primaryRef` into two computeds. The new `primaryRefFilter` drives a second `PlDropdownRef` in Settings; `createPrimaryRef(col, filter)` rebuilds the envelope on either change. Workflow: `wf.prepare` adds the filter as a `bb.addSingle(..., "filter")` when set, sharing the PDB anchor's scClonotypeKey axis. `wf.body` exports the resolved column via `xsv.exportFrame` and stages it as `clonotype_filter.tsv` with a new `--clonotype-filter` CLI flag. Python: loads the TSV into a `keep_clonotypes` set, parsing common falsy spellings (`0`, `false`, `no`, `null`, empty, `0.0`). Skips clonotypes outside the set before iteration. No-op when the user didn't pick a filter (the in-block path stays the default).
1 parent 9bc9517 commit cfcfc72

4 files changed

Lines changed: 90 additions & 10 deletions

File tree

model/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,18 @@ export const platforma = BlockModelV3.create(dataModel)
286286
},
287287
]),
288288
)
289+
// Spec R1 `PrimaryRef.filter` , surface Int/Boolean PColumns anchored on
290+
// the same scClonotypeKey axis as the PDB column so the user can pick
291+
// a subset filter (e.g. upstream `pl7.app/structure/predictionSuccessful`
292+
// or `pl7.app/structure/confident`). The workflow stages whatever the
293+
// user picks as a TSV sidecar and Python drops clonotypes whose value
294+
// is falsy before iterating.
295+
.output("filterOptions", (ctx) =>
296+
ctx.resultPool.getOptions([
297+
{ name: "pl7.app/structure/predictionSuccessful" },
298+
{ name: "pl7.app/structure/confident" },
299+
]),
300+
)
289301
// Spec R51 , per-clonotype scalar metrics table. PColumns come from the
290302
// PrimaryRef-path `scoresData` PFrame (axes: [scClonotypeKey]). Hidden
291303
// on the legacy single-PDB path (`scoresData` not emitted; resolve

software/liabilities-script/main.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,12 @@ def main() -> None:
322322
"provided, the per-clonotype value overrides the "
323323
"in-block CDR3 Cα count as the R30 compactness "
324324
"numerator (R5 / R29).")
325+
ap.add_argument("--clonotype-filter", type=Path, default=None,
326+
dest="clonotype_filter_tsv",
327+
help="Optional TSV exported from a Boolean/Int PColumn "
328+
"(spec R1 `PrimaryRef.filter`). Clonotypes whose "
329+
"value is falsy (0, false, empty) are skipped "
330+
"before iteration.")
325331
args = ap.parse_args()
326332

327333
if not args.pdb_dir.is_dir():
@@ -354,6 +360,28 @@ def main() -> None:
354360
except (TypeError, ValueError):
355361
continue
356362

363+
keep_clonotypes: set[str] | None = None
364+
if args.clonotype_filter_tsv is not None and args.clonotype_filter_tsv.is_file():
365+
with args.clonotype_filter_tsv.open() as fh:
366+
reader = csv.DictReader(fh, delimiter="\t")
367+
key_col = next((c for c in (reader.fieldnames or []) if "scClonotypeKey" in c), None)
368+
val_col = next(
369+
(c for c in (reader.fieldnames or []) if c != key_col),
370+
None,
371+
)
372+
if key_col and val_col:
373+
keep_clonotypes = set()
374+
for r in reader:
375+
raw = (r.get(val_col) or "").strip().lower()
376+
if raw in {"", "0", "false", "no", "null"}:
377+
continue
378+
try:
379+
if float(raw) == 0.0:
380+
continue
381+
except ValueError:
382+
pass
383+
keep_clonotypes.add(r[key_col])
384+
357385
out_buf = StringIO()
358386
writer = csv.writer(out_buf, delimiter="\t", lineterminator="\n")
359387
writer.writerow(_TSV_COLUMNS)
@@ -365,6 +393,8 @@ def main() -> None:
365393
f"clonotypeKey<TAB>filename): {entry}"
366394
)
367395
clonotype_key, pdb_filename = entry
396+
if keep_clonotypes is not None and clonotype_key not in keep_clonotypes:
397+
continue
368398
pdb_path = args.pdb_dir / pdb_filename
369399
if not pdb_path.is_file():
370400
raise SystemExit(

ui/src/pages/MainPage.vue

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,25 @@ const { runSummary, showRedAlert, showGatedAlert } = useRunSummaryAlerts(scoresT
9494
const settingsOpen = ref(!app.model.data.primaryRef?.column);
9595
9696
// Spec R1 , `PrimaryRef` is a frozen `{__isPrimaryRef, column, filter?}`
97-
// envelope. `PlDropdownRef` deals in plain `PlRef`, so we expose the
98-
// inner `column` to the dropdown and rebuild the envelope on every
99-
// change via `createPrimaryRef`. The filter slot (R47) stays
100-
// `undefined` until subset selection is wired.
97+
// envelope. `PlDropdownRef` deals in plain `PlRef`, so we split into two
98+
// computeds: the primary column drives the dropdown over `pdbOptions`,
99+
// the optional filter PlRef narrows the clonotype set at workflow time.
100+
// Setting the column rebuilds the envelope; setting the filter merges it
101+
// onto the existing envelope (or no-ops if there's no column yet).
101102
const primaryRefColumn = computed<PlRef | undefined>({
102103
get: () => app.model.data.primaryRef?.column,
103104
set: (value) => {
104-
app.model.data.primaryRef = value ? createPrimaryRef(value) : undefined;
105+
app.model.data.primaryRef = value
106+
? createPrimaryRef(value, app.model.data.primaryRef?.filter)
107+
: undefined;
108+
},
109+
});
110+
const primaryRefFilter = computed<PlRef | undefined>({
111+
get: () => app.model.data.primaryRef?.filter,
112+
set: (value) => {
113+
const col = app.model.data.primaryRef?.column;
114+
if (!col) return;
115+
app.model.data.primaryRef = createPrimaryRef(col, value);
105116
},
106117
});
107118
@@ -170,6 +181,19 @@ const modalTitle = computed(() => {
170181
clearable
171182
/>
172183

184+
<!-- Spec R1 `PrimaryRef.filter` , subset of clonotypes to analyze.
185+
Compatible columns auto-discovered from the result pool
186+
(`pl7.app/structure/predictionSuccessful`, `confident`); when
187+
set, the workflow exports the column to a TSV sidecar and
188+
Python skips clonotypes whose value is falsy. Optional; leave
189+
clear to analyze every clonotype in the dataset. -->
190+
<PlDropdownRef
191+
v-model="primaryRefFilter"
192+
:options="app.model.outputs.filterOptions ?? []"
193+
label="Clonotype filter (optional)"
194+
clearable
195+
/>
196+
173197
<div
174198
:style="{
175199
display: 'grid',

workflow/src/main.tpl.tengo

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ liabilitiesSw := assets.importSoftware("@platforma-open/milabs.3d-structure-base
2727

2828
wf.prepare(func(args) {
2929
// Anchor on the PDB column so upstream enrichments sharing the
30-
// scClonotypeKey axis (R2) auto-discover. R5/R29: cdrh3Length from
31-
// upstream feeds the R30 compactness numerator instead of our
32-
// REMARK 99 / scheme-fallback count. Optional: when the prediction
33-
// block in the chain is older and doesn't emit cdrh3Length, the
34-
// bundle yields no column and Python falls back to its in-block count.
30+
// scClonotypeKey axis (R2) auto-discover. R5/R29: cdrh3Length feeds
31+
// the R30 compactness numerator. R1 PrimaryRef.filter: when the
32+
// user picks a filter PlRef, resolve it as a Single so wf.body can
33+
// export it to a TSV sidecar; Python then skips clonotypes whose
34+
// filter value is falsy.
3535
bb := wf.createPBundleBuilder()
3636
bb.ignoreMissingDomains()
3737
bb.addAnchor("pdb", args.primaryRef.column)
@@ -41,6 +41,9 @@ wf.prepare(func(args) {
4141
name: "pl7.app/structure/cdrh3Length"
4242
},
4343
"cdrh3Length")
44+
if !is_undefined(args.primaryRef.filter) {
45+
bb.addSingle(args.primaryRef.filter, "filter")
46+
}
4447

4548
return {
4649
resolvedPdb: wf.resolve(args.primaryRef.column, { errIfMissing: true }),
@@ -106,6 +109,17 @@ wf.body(func(args) {
106109
arg("--cdrh3-lengths").arg("cdrh3_lengths.tsv")
107110
}
108111

112+
// R1 PrimaryRef.filter: stage the picked filter column as a TSV
113+
// sidecar; Python drops clonotypes whose filter value is falsy
114+
// before iterating. Empty when the user didn't pick a filter.
115+
filterCol := args.columns.getColumn("filter")
116+
if !is_undefined(filterCol) {
117+
filterTsv := xsv.exportFrame([filterCol], "tsv", { mem: "1GiB", cpu: 1 })
118+
cmd = cmd.
119+
addFile("clonotype_filter.tsv", filterTsv).
120+
arg("--clonotype-filter").arg("clonotype_filter.tsv")
121+
}
122+
109123
// Optional CLI knobs (R10 fallback path + R9 chain identity overrides).
110124
// Empty strings are skipped so Python falls through to its defaults.
111125
if !is_undefined(args.numberingScheme) && args.numberingScheme != "" {

0 commit comments

Comments
 (0)