diff --git a/.changeset/tangy-snails-slide.md b/.changeset/tangy-snails-slide.md new file mode 100644 index 0000000..bf0a871 --- /dev/null +++ b/.changeset/tangy-snails-slide.md @@ -0,0 +1,8 @@ +--- +"@platforma-open/milaboratories.redefine-clonotypes.anarci-numbering": minor +"@platforma-open/milaboratories.redefine-clonotypes.workflow": minor +"@platforma-open/milaboratories.redefine-clonotypes.model": minor +"@platforma-open/milaboratories.redefine-clonotypes.ui": minor +--- + +Introduce per position AA export to support AA abundance plot diff --git a/model/src/index.ts b/model/src/index.ts index 5fe274b..3a8ef9a 100644 --- a/model/src/index.ts +++ b/model/src/index.ts @@ -47,6 +47,7 @@ export type BlockArgs = { numberingScheme?: 'imgt' | 'kabat' | 'chothia'; mem?: number; cpu?: number; + exportCdr3AaPositions?: boolean; }; export const model = BlockModel.create() diff --git a/software/anarci-numbering/src/main.py b/software/anarci-numbering/src/main.py index 82070a9..2164cb4 100644 --- a/software/anarci-numbering/src/main.py +++ b/software/anarci-numbering/src/main.py @@ -280,6 +280,7 @@ def main() -> None: p.add_argument("--cdr_mapping_kl", required=False, help="CDR annotation mapping JSON for light chain") p.add_argument("--out_tsv", required=True, help="Output TSV path") p.add_argument("--stats_tsv", required=False, help="Output numbering stats TSV path") + p.add_argument("--positions_tsv", required=False, help="Output per-position CDR3 amino acid TSV path") args = p.parse_args() keys, seqs, fields = read_input_tsv(args.input_tsv) @@ -361,6 +362,31 @@ def main() -> None: } pl.DataFrame(stats_data).write_csv(args.stats_tsv, separator="\t") + # Write per-position CDR3 amino acid data + if args.positions_tsv: + chain_domain = {"H": "IGHeavy", "KL": "IGLight"} + pos_rows: List[List[str]] = [] + unique_keys = list(dict.fromkeys(keys)) + for key in unique_keys: + for chain in chains: + anarci_rows, pos_labels = anarci_by_chain[chain] + residues = anarci_rows.get(key) if anarci_rows and pos_labels else None + if residues is None: + continue + ranges = REGION_RANGES[args.scheme][chain] + cdr3_start, cdr3_end = ranges["CDR3"] + for pos_label, residue in zip(pos_labels, residues): + num = position_number(pos_label) + if num is None or num < cdr3_start or num > cdr3_end: + continue + residue = (residue or "").strip() + if residue in {"", "-", "."}: + continue + pos_rows.append([key, chain_domain[chain], pos_label, residue, "1"]) + pl.DataFrame( + pos_rows, schema=["clonotypeKey", "chain", "position", "aminoacid", "count"], orient="row" + ).write_csv(args.positions_tsv, separator="\t") + if __name__ == "__main__": main() diff --git a/ui/src/pages/MainPage.vue b/ui/src/pages/MainPage.vue index 848a861..ecff45a 100644 --- a/ui/src/pages/MainPage.vue +++ b/ui/src/pages/MainPage.vue @@ -1,6 +1,6 @@