diff --git a/AGENTS.md b/AGENTS.md index 1e4b755..5ac79cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,8 @@ Your job is to run analysis and consume **structured results**, not to edit the | `LAMBS.analyzeMabReport(vhSeq, vlSeq)` | Report from **cleaned** AA strings | | `LAMBS.buildChainReport(varSeq, 'VH' \| 'VL')` | Variable-region chain report | | `LAMBS.buildClusterReport(basket, threshold, filterNames)` | Cluster report | +| `LAMBS.getHumanIgGEuNumbers(isotype)` | Eu index array for `IGHG1`–`IGHG4`, else `null` (IMGT Hu_IGHGnber) | +| `LAMBS.mapEuOntoConstantAlignment(aligned2, species, isotype)` | Per-column Eu numbers for a CH alignment, or `null` | | `LAMBS.lambsReportToJSON(report)` | JSON string (no `_constMatch` / alignment blobs) | | `LAMBS.lastReport` | Last single-tab report after **Analyze** | | `LAMBS.lastClusterReport` | Last cluster report after **Cluster & Analyze** | diff --git a/README.md b/README.md index 14961a6..826164f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Code is open source, permissively licensed under Apache 2.0, and available at [h - **Developability liability identification**: deamidation, isomerization, glycosylation, unpaired cysteines, and more. - **Germline gene identification and alignment**: human and mouse with annotation of somatic hypermutation and CDRs. - **VH and VL chain properties**: length, molecular weight, theoretical pI, net charge at pH 7.4 and 5.5, and GRAVY with percentiles and quartiles versus approved therapeutic mAb sequences. -- **Constant region identification**: human and mouse with alignments showing similarity to wildtype constant regions. +- **Constant region identification**: human and mouse with alignments showing similarity to wildtype constant regions. Human IgG (IGHG1–4) CH alignments include an EU numbering row above the raw residue ruler, after isotype determination. - **Batch analysis**: clonal-family clustering by V gene, J gene, CDR3 length, and adjustable CDR3 similarity with multiple sequence alignment, AA conservation plots, and consensus sequences per family. Accepts CSV upload. - **Verified analysis logic**: core algorithms are covered by extensive unit tests; run locally with `node test.js`. - **Privacy**: everything runs locally in the browser. No tracking and no data is sent anywhere. diff --git a/index.html b/index.html index 395be69..bd42ce2 100644 --- a/index.html +++ b/index.html @@ -2850,6 +2850,101 @@

Sequence Basket

return chars; } + // ================================================================ + // HUMAN IgG EU NUMBERING (Edelman / EU index) + // ================================================================ + // Correspondence tables from IMGT Scientific chart: + // https://imgt.org/IMGTScientificChart/Numbering/Hu_IGHGnber.html + // Arrays are parallel to CONST_REGION_DB.human.CH[isotype].s (one + // entry per mature-protein residue). null = residue present in the + // isotype but without an Eu index (IgG3 extended hinge). + // Hinge Eu indices differ by isotype (IgG1 continuous 216–230; + // IgG2/IgG4 skip residues; IgG3 mostly unnumbered outside H1/H4). + // IgG2 CH2 also omits Eu 233 (IMGT note: codon 1.4 deletion). + const HUMAN_IGHG_EU_NUMBERING = (function () { + const ch1 = []; + for (let n = 118; n <= 215; n++) ch1.push(n); + const ch3 = []; + for (let n = 341; n <= 447; n++) ch3.push(n); + const ch2Full = []; + for (let n = 231; n <= 340; n++) ch2Full.push(n); + const ch2IgG2 = [231, 232]; + for (let n = 234; n <= 340; n++) ch2IgG2.push(n); + const hinge1 = []; + for (let n = 216; n <= 230; n++) hinge1.push(n); + const hinge2 = [216, 217, 218, 219, 220, 222, 224, 226, 227, 228, 229, 230]; + const hinge4 = [216, 217, 218, 219, 220, 224, 225, 226, 227, 228, 229, 230]; + const hinge3 = [216, 217, 218, null, null, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, null, null]; + for (let i = 0; i < 15; i++) hinge3.push(null); // H2 + for (let i = 0; i < 15; i++) hinge3.push(null); // H3 + for (let i = 0; i < 13; i++) hinge3.push(null); // H4 prefix + hinge3.push(229, 230); + return { + IGHG1: ch1.concat(hinge1, ch2Full, ch3), + IGHG2: ch1.concat(hinge2, ch2IgG2, ch3), + IGHG3: ch1.concat(hinge3, ch2Full, ch3), + IGHG4: ch1.concat(hinge4, ch2Full, ch3) + }; + })(); + + /** Eu index array for a human IGHG isotype, or null if not applicable. */ + function getHumanIgGEuNumbers(isotype) { + if (!isotype) return null; + return HUMAN_IGHG_EU_NUMBERING[isotype] || null; + } + + /** + * Map Eu numbers onto constant-region alignment columns using the + * reference row (aligned2). Gaps in the reference (query insertions / + * free suffix) get null. Only for human IGHG1–4. + */ + function mapEuOntoConstantAlignment(aligned2, species, isotype) { + if (species !== 'human') return null; + const refEu = getHumanIgGEuNumbers(isotype); + if (!refEu || !aligned2) return null; + const out = new Array(aligned2.length); + let ri = 0; + for (let i = 0; i < aligned2.length; i++) { + if (aligned2[i] === '-') { + out[i] = null; + } else { + out[i] = ri < refEu.length ? refEu[ri] : null; + ri++; + } + } + return out; + } + + /** + * Build a ruler string for Eu indices: label columns whose Eu number + * is 1 or a multiple of 10 (right-aligned, same convention as buildRuler). + * Columns with null Eu stay blank. + */ + function buildEuRuler(euPerColumn) { + const length = euPerColumn.length; + const chars = new Array(length).fill(' '); + for (let i = 0; i < length; i++) { + const eu = euPerColumn[i]; + if (eu == null) continue; + if (eu !== 1 && eu % 10 !== 0) continue; + const num = String(eu); + for (let k = 0; k < num.length; k++) { + const c = i - (num.length - 1) + k; + if (c >= 0 && c < length) chars[c] = num[k]; + } + } + return chars; + } + + function renderEuNumberingRow(euPerColumn, labelClass) { + const ruler = buildEuRuler(euPerColumn); + let h = '
EU:'; + for (let i = 0; i < ruler.length; i++) { + h += '' + ruler[i] + ''; + } + return h + '
'; + } + // ================================================================ // SEQUENCE LIABILITY DETECTION // ================================================================ @@ -3947,6 +4042,10 @@

Sequence Basket

let refBody = '
'; + const euCols = mapEuOntoConstantAlignment( + constMatch.aligned2, constMatch.species, constMatch.isotype + ); + if (euCols) refBody += renderEuNumberingRow(euCols, lbl); refBody += renderRulerRow(alen, lbl); refBody += renderGermlineGeneRowHtml(lbl, { gene: constMatch.isotype }, constMatch.aligned2, colSpec); refBody += renderGermlineMismatchRowHtml(lbl, constMatch.aligned1, constMatch.aligned2, colSpec); @@ -5387,6 +5486,8 @@

Sequence Basket

analyzeMabReportFromRaw: analyzeMabReportFromRaw, buildClusterReport: buildClusterReport, lambsReportToJSON: lambsReportToJSON, + getHumanIgGEuNumbers: getHumanIgGEuNumbers, + mapEuOntoConstantAlignment: mapEuOntoConstantAlignment, lastReport: null, lastClusterReport: null, analyzeSingle: function (vhRaw, vlRaw) { diff --git a/test.js b/test.js index e5582a6..410ac35 100644 --- a/test.js +++ b/test.js @@ -88,6 +88,12 @@ const wrappedJS = ` exports.alignConstantRegion = alignConstantRegion; exports.findConstantRegion = findConstantRegion; exports.CONST_REGION_DB = CONST_REGION_DB; + exports.HUMAN_IGHG_EU_NUMBERING = HUMAN_IGHG_EU_NUMBERING; + exports.getHumanIgGEuNumbers = getHumanIgGEuNumbers; + exports.mapEuOntoConstantAlignment = mapEuOntoConstantAlignment; + exports.buildEuRuler = buildEuRuler; + exports.renderEuNumberingRow = renderEuNumberingRow; + exports.renderConstantRegionPanel = renderConstantRegionPanel; exports.EXAMPLE_CSV = EXAMPLE_CSV; exports.csvChainColumnIndices = csvChainColumnIndices; exports.alignJGene = alignJGene; @@ -148,6 +154,12 @@ const { alignConstantRegion, findConstantRegion, CONST_REGION_DB, + HUMAN_IGHG_EU_NUMBERING, + getHumanIgGEuNumbers, + mapEuOntoConstantAlignment, + buildEuRuler, + renderEuNumberingRow, + renderConstantRegionPanel, EXAMPLE_CSV, csvChainColumnIndices, alignJGene, @@ -2957,6 +2969,375 @@ assert( findConstantRegion(TEST_VAR_REGION + truncatedCHPrefix) === null, "findConstantRegion: returns null when only a short CH prefix is present", ); + +// ============================================================ +// Human IgG EU numbering (Edelman / EU index via IMGT correspondence) +// Provenance: IMGT Scientific chart Hu_IGHGnber.html; therapeutic +// sequences from PDB / KEGG / DDInter as cited per case. +// ============================================================ +section("Human IgG EU numbering"); + +function aaAtEu(isotype, seq, euNumber) { + const eu = getHumanIgGEuNumbers(isotype); + const idx = eu.indexOf(euNumber); + return idx >= 0 ? seq[idx] : null; +} + +// --- Map integrity vs CONST_REGION_DB and IMGT domain lengths --- +for (const iso of ["IGHG1", "IGHG2", "IGHG3", "IGHG4"]) { + const seq = CONST_REGION_DB.human.CH[iso].s; + const eu = getHumanIgGEuNumbers(iso); + assert(eu !== null, `getHumanIgGEuNumbers(${iso}): returns map`); + assert( + eu.length === seq.length, + `EU map length matches CONST_REGION_DB ${iso}: ${eu.length} vs ${seq.length}`, + ); + assert( + HUMAN_IGHG_EU_NUMBERING[iso] === eu, + `HUMAN_IGHG_EU_NUMBERING.${iso} is the same array as getHumanIgGEuNumbers`, + ); +} + +assert( + getHumanIgGEuNumbers("IGHA1") === null, + "getHumanIgGEuNumbers: null for non-IgG isotype", +); +assert( + getHumanIgGEuNumbers("IGHM") === null, + "getHumanIgGEuNumbers: null for IGHM", +); +assert( + mapEuOntoConstantAlignment("ASTK", "mouse", "IGHG1") === null, + "mapEuOntoConstantAlignment: null for mouse IgG", +); +assert( + mapEuOntoConstantAlignment("ASTK", "human", "IGKC") === null, + "mapEuOntoConstantAlignment: null for human light-chain constant", +); + +// CH1 always Eu 118–215 (98 aa) for all human IGHG +for (const iso of ["IGHG1", "IGHG2", "IGHG3", "IGHG4"]) { + const eu = getHumanIgGEuNumbers(iso); + assert(eu[0] === 118, `${iso} Eu[0] = 118 (CH1 start)`); + assert(eu[97] === 215, `${iso} Eu[97] = 215 (CH1 end)`); + assert( + CONST_REGION_DB.human.CH[iso].s[0] === "A", + `${iso} CH1 start residue A118`, + ); +} + +// IGHG1 continuous hinge 216–230, CH2 231–340, CH3 341–447 +{ + const eu = getHumanIgGEuNumbers("IGHG1"); + const g1 = CONST_REGION_DB.human.CH.IGHG1.s; + assert(eu[98] === 216 && eu[112] === 230, "IGHG1 hinge Eu 216–230"); + assert(eu[113] === 231 && eu[222] === 340, "IGHG1 CH2 Eu 231–340"); + assert(eu[223] === 341 && eu[329] === 447, "IGHG1 CH3 Eu 341–447"); + // Classic Eu landmarks (Edelman Eu protein / IMGT table) + assert(aaAtEu("IGHG1", g1, 118) === "A", "IGHG1 A118"); + assert(aaAtEu("IGHG1", g1, 216) === "E", "IGHG1 E216 (hinge start)"); + assert(aaAtEu("IGHG1", g1, 226) === "C", "IGHG1 C226 (hinge Cys)"); + assert(aaAtEu("IGHG1", g1, 234) === "L", "IGHG1 L234"); + assert(aaAtEu("IGHG1", g1, 297) === "N", "IGHG1 N297 (N-glycan)"); + assert(aaAtEu("IGHG1", g1, 329) === "P", "IGHG1 P329"); + assert(aaAtEu("IGHG1", g1, 446) === "G", "IGHG1 G446"); + assert(aaAtEu("IGHG1", g1, 447) === "K", "IGHG1 K447"); +} + +// IGHG2: hinge skips Eu 221/223/225; CH2 omits Eu 233 (IMGT note 2) +{ + const eu = getHumanIgGEuNumbers("IGHG2"); + const g2 = CONST_REGION_DB.human.CH.IGHG2.s; + assert( + JSON.stringify(eu.slice(98, 110)) === + JSON.stringify([216, 217, 218, 219, 220, 222, 224, 226, 227, 228, 229, 230]), + "IGHG2 hinge Eu indices match IMGT IGHG2 hinge table", + ); + assert(eu.indexOf(233) === -1, "IGHG2 has no Eu 233 (CH2 codon 1.4 deletion)"); + assert(aaAtEu("IGHG2", g2, 231) === "A", "IGHG2 A231"); + assert(aaAtEu("IGHG2", g2, 232) === "P", "IGHG2 P232"); + assert(aaAtEu("IGHG2", g2, 234) === "P", "IGHG2 P234 (after skipped 233)"); + assert(aaAtEu("IGHG2", g2, 297) === "N", "IGHG2 N297"); + assert(aaAtEu("IGHG2", g2, 226) === "C", "IGHG2 C226"); +} + +// IGHG4: hinge Eu 216–220 then 224–230 (skips 221–223) +{ + const eu = getHumanIgGEuNumbers("IGHG4"); + const g4 = CONST_REGION_DB.human.CH.IGHG4.s; + assert( + JSON.stringify(eu.slice(98, 110)) === + JSON.stringify([216, 217, 218, 219, 220, 224, 225, 226, 227, 228, 229, 230]), + "IGHG4 hinge Eu indices match IMGT IGHG4 hinge table", + ); + assert(eu.indexOf(221) === -1 && eu.indexOf(222) === -1 && eu.indexOf(223) === -1, + "IGHG4 hinge omits Eu 221–223"); + assert(aaAtEu("IGHG4", g4, 228) === "S", "IGHG4 wild-type S228"); + assert(aaAtEu("IGHG4", g4, 234) === "F", "IGHG4 F234 (APEFLGG)"); + assert(aaAtEu("IGHG4", g4, 297) === "N", "IGHG4 N297"); +} + +// IGHG3: extended hinge — only H1/H4 carry Eu indices per IMGT +{ + const eu = getHumanIgGEuNumbers("IGHG3"); + const g3 = CONST_REGION_DB.human.CH.IGHG3.s; + assert(eu[98] === 216 && eu[100] === 218, "IGHG3 H1 Eu 216–218"); + assert(eu[101] === null && eu[102] === null, "IGHG3 H1 positions 4–5 unnumbered"); + assert(eu[103] === 219 && eu[112] === 228, "IGHG3 H1 Eu 219–228"); + assert(eu[113] === null && eu[114] === null, "IGHG3 H1 positions 16–17 unnumbered"); + // H2+H3 (30 residues) all null + for (let i = 115; i < 145; i++) { + assert(eu[i] === null, `IGHG3 hinge H2/H3 index ${i} unnumbered`); + } + assert(eu[158] === 229 && eu[159] === 230, "IGHG3 H4 Eu 229–230"); + assert(eu[160] === 231, "IGHG3 CH2 starts at Eu 231"); + assert(aaAtEu("IGHG3", g3, 297) === "N", "IGHG3 N297"); + assert(g3[98] === "E" && g3[159] === "P", "IGHG3 hinge termini E…P"); +} + +// --- Alignment mapping: perfect match --- +{ + const g1 = CONST_REGION_DB.human.CH.IGHG1.s; + const mapped = mapEuOntoConstantAlignment(g1, "human", "IGHG1"); + assert(mapped !== null && mapped.length === g1.length, "mapEu perfect IGHG1 length"); + assert(mapped[0] === 118 && mapped[mapped.length - 1] === 447, "mapEu perfect termini"); + const n297col = mapped.indexOf(297); + assert(n297col >= 0 && g1[n297col] === "N", "mapEu perfect N297 column"); +} + +// Alignment with a query insertion (gap in reference): Eu stays on ref residues +{ + const g1 = CONST_REGION_DB.human.CH.IGHG1.s; + const a2 = g1.slice(0, 5) + "-" + g1.slice(5); + const mapped = mapEuOntoConstantAlignment(a2, "human", "IGHG1"); + assert(mapped[5] === null, "mapEu: gap in reference → null Eu"); + assert(mapped[4] === 122 && mapped[6] === 123, "mapEu: Eu continues across ref gap"); +} + +// buildEuRuler labels multiples of 10 +{ + const mapped = mapEuOntoConstantAlignment( + CONST_REGION_DB.human.CH.IGHG1.s, + "human", + "IGHG1", + ); + const ruler = buildEuRuler(mapped); + const col120 = mapped.indexOf(120); + const col300 = mapped.indexOf(300); + assert(ruler.join("").includes("120"), "buildEuRuler includes 120"); + assert(ruler.join("").includes("300"), "buildEuRuler includes 300"); + assert(ruler[col120] === "0", "buildEuRuler right-aligns 120 on column"); + assert(ruler[col300] === "0", "buildEuRuler right-aligns 300 on column"); +} + +// Panel HTML: EU row above raw ruler for human IgG; absent otherwise +{ + const hit = findConstantRegion(TEST_VAR_REGION + HUMAN_IGHG1_CH); + const html = renderConstantRegionPanel(hit, "CH"); + const euIdx = html.indexOf(">EU:"); + const rawRulerIdx = html.indexOf('class="ruler-char">1'); + assert(euIdx >= 0, "renderConstantRegionPanel: EU row present for human IGHG1"); + assert( + euIdx < rawRulerIdx, + "renderConstantRegionPanel: EU row appears above raw numbering", + ); + // Digits are one-per-span (same as raw ruler); check reconstructed EU text + const euRowMatch = html.match(/>EU:<\/span>([\s\S]*?)<\/div>/); + assert(euRowMatch, "EU row HTML parseable"); + const euText = euRowMatch[1].replace(/<[^>]+>/g, ""); + assert(euText.includes("120") && euText.includes("300"), + "EU row shows Eu decade labels (120, 300)"); +} +{ + const hit = findConstantRegion(TEST_VAR_REGION + HUMAN_IGKC_CL); + const html = renderConstantRegionPanel(hit, "CL"); + assert( + !html.includes(">EU:"), + "renderConstantRegionPanel: no EU row for IGKC", + ); +} + +// --- Real therapeutics (provenance in comments) --- + +// Trastuzumab (Herceptin) IgG1κ — heavy chain from KEGG DRUG D03257 +// (https://www.kegg.jp/entry/D03257). CH matches human IGHG1 with +// CH3 D356E/L358M allotype (REEM vs RDEL). Disulfide note lists +// H229–H'229 / H232–H'232 (Kabat hinge Cys; Eu 226 / 229). +const TRASTUZUMAB_HC = + "EVQLVESGGGLVQPGGSLRLSCAASGFNIKDTYIHWVRQAPGKGLEWVARIYPTNGYTRY" + + "ADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCSRWGGDGFYAMDYWGQGTLVTVSS" + + "ASTKGPSVFPLAPSSKSTSGGTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSS" + + "GLYSLSSVVTVPSSSLGTQTYICNVNHKPSNTKVDKKVEPKSCDKTHTCPPCPAPELLGG" + + "PSVFLFPPKPKDTLMISRTPEVTCVVVDVSHEDPEVKFNWYVDGVEVHNAKTKPREEQYN" + + "STYRVVSVLTVLHQDWLNGKEYKCKVSNKALPAPIEKTISKAKGQPREPQVYTLPPSREE" + + "MTKNQVSLTCLVKGFYPSDIAVEWESNGQPENNYKTTPPVLDSDGSFFLYSKLTVDKSRW" + + "QQGNVFSCSVMHEALHNHYTQKSLSLSPG"; +{ + const hit = findConstantRegion(TRASTUZUMAB_HC); + assert(hit && hit.isotype === "IGHG1" && hit.species === "human", + "trastuzumab: identified as human IGHG1"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + assert(euCols, "trastuzumab: Eu columns mapped"); + // N297 glycosylation site (standard Eu index for IgG Fc N-glycan) + const n297 = euCols.indexOf(297); + assert(n297 >= 0 && hit.aligned1[n297] === "N", + "trastuzumab: query residue at Eu 297 is N"); + // Hinge Cys Eu 226 / 229 (KEGG disulfide H229/H232 Kabat ≈ Eu 226/229) + assert(hit.aligned1[euCols.indexOf(226)] === "C", "trastuzumab: C226"); + assert(hit.aligned1[euCols.indexOf(229)] === "C", "trastuzumab: C229"); + assert(hit.aligned1[euCols.indexOf(234)] === "L", "trastuzumab: L234"); + // Truncated vs DB at C-terminus (KEGG ends …SPG without K447) + assert(euCols.indexOf(447) >= 0 && hit.aligned1[euCols.indexOf(447)] === "-", + "trastuzumab: Eu 447 is a gap in query (KEGG sequence omits K447)"); + const html = renderConstantRegionPanel(hit, "CH"); + assert(html.includes(">EU:"), "trastuzumab panel includes EU row"); +} + +// Adalimumab Fab heavy (PDB 3WD5 chain H) — CH1 through …VDKKI +// https://www.rcsb.org/structure/3WD5 +// Full-length therapeutic VH from igblast_thera cdr3TestCases (Adalimumab_vh). +const ADALIMUMAB_FAB_HC = + "EVQLVESGGGLVQPGRSLRLSCAASGFTFDDYAMHWVRQAPGKGLEWVSAITWNSGHIDYADSVEGRFTISRDNAKNSLYLDMNSLRAEDTAVYYCAKVSYLSTASSLDYWGQGTLVTVSS" + + "ASTKGPSVFPLAPSSKSTSGGTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSSLGTQTYICNVNHKPSNTKVDKKI"; +{ + const adaVh = cdr3TestCases.find(function (tc) { return tc.id === "Adalimumab_vh"; }).seq; + const adaFull = adaVh + HUMAN_IGHG1_CH; + const hit = findConstantRegion(adaFull); + assert(hit && hit.isotype === "IGHG1", "adalimumab VH+IGHG1: IGHG1"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + assert(hit.aligned1[euCols.indexOf(118)] === "A", "adalimumab: A118"); + assert(hit.aligned1[euCols.indexOf(297)] === "N", "adalimumab: N297"); + const fabCH1 = ADALIMUMAB_FAB_HC.slice(ADALIMUMAB_FAB_HC.indexOf("ASTKG")); + // PDB 3WD5 Fab ends at CH1 with I215 (vs germline V215); first 97 aa are identical + assert(fabCH1.slice(0, 97) === HUMAN_IGHG1_CH.slice(0, 97), + "adalimumab Fab CH1 (PDB 3WD5) matches IGHG1 Eu 118–214"); + assert(fabCH1[97] === "I" && HUMAN_IGHG1_CH[97] === "V", + "adalimumab Fab CH1 ends I215 (allotype/construct) vs DB V215"); +} + +// Rituximab Fab heavy (PDB 2OSL) — murine VH + human IGHG1 CH1/hinge start +// https://www.rcsb.org/structure/2OSL +const RITUXIMAB_FAB_HC = + "QVQLQQPGAELVKPGASVKMSCKASGYTFTSYNMHWVKQTPGRGLEWIGAIYPGNGDTSYNQKFKGKATLTADKSSSTAYMQLSSLTSEDSAVYYCARSTYYGGDWYFNVWGAGTTVTVSA" + + "ASTKGPSVFPLAPSSKSTSGGTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSSLGTQTYICNVNHKPSNTKVDKKVEPKSC"; +{ + // Fab includes CH1 + hinge through Eu 220 (…VEPKSC); DB index 0–103 is …VEPKSCD + assert( + RITUXIMAB_FAB_HC.includes(HUMAN_IGHG1_CH.slice(0, 103)), + "rituximab Fab (PDB 2OSL): human IGHG1 through Eu 220 (…EPKSC)", + ); + const full = RITUXIMAB_FAB_HC + HUMAN_IGHG1_CH.slice(103); + const hit = findConstantRegion(full); + assert(hit && hit.isotype === "IGHG1", "rituximab: IGHG1"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + assert(hit.aligned1[euCols.indexOf(220)] === "C", "rituximab: C220"); + assert(hit.aligned1[euCols.indexOf(226)] === "C", "rituximab: C226"); +} + +// b12 broadly neutralizing Ab IgG1 — full HC from PDB 1HZH +// https://www.rcsb.org/structure/1HZH (Saphire et al.) +const B12_HC = + "QVQLVQSGAEVKKPGASVKVSCQASGYRFSNFVIHWVRQAPGQRFEWMGWINPYNGNKEFSAKFQDRVTFTADTSANTAYMELRSLRSADTAVYYCARVGPYSWDDSPQDNYYMDVWGKGTTVIVSS" + + "ASTKGPSVFPLAPSSKSTSGGTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSSLGTQTYICNVNHKPSNTKVDKKAEPKSCDKTHTCPPCPAPELLGGPSVFLFPPKPKDTLMISRTPEVTCVVVDVSHEDPEVKFNWYVDGVEVHNAKTKPREEQYNSTYRVVSVLTVLHQDWLNGKEYKCKVSNKALPAPIEKTISKAKGQPREPQVYTLPPSRDELTKNQVSLTCLVKGFYPSDIAVEWESNGQPENNYKTTPPVLDSDGSFFLYSKLTVDKSRWQQGNVFSCSVMHEALHNHYTQKSLSLSPGK"; +{ + const hit = findConstantRegion(B12_HC); + assert(hit && hit.isotype === "IGHG1", "b12 (1HZH): IGHG1"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + // Allotype: A215 (Kabat/Eu CH1 end) vs germline V215 — PDB has A before EPKSC + assert(hit.aligned1[euCols.indexOf(215)] === "A", "b12: A215 allotype"); + assert(hit.aligned1[euCols.indexOf(297)] === "N", "b12: N297"); + assert(hit.aligned1[euCols.indexOf(447)] === "K", "b12: K447"); +} + +// Pembrolizumab IgG4 S228P — heavy chain from PDB 5DK3 +// https://www.rcsb.org/structure/5DK3 ; S228P is the hinge-stabilizing mutation +// (InvivoGen / literature: Ser→Pro at Eu 228). +const PEMBROLIZUMAB_HC = + "QVQLVQSGVEVKKPGASVKVSCKASGYTFTNYYMYWVRQAPGQGLEWMGGINPSNGGTNFNEKFKNRVTLTTDSSTTTAYMELKSLQFDDTAVYYCARRDYRFDMGFDYWGQGTTVTVSS" + + "ASTKGPSVFPLAPCSRSTSESTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSSLGTKTYTCNVDHKPSNTKVDKRVESKYGPPCPPCPAPEFLGGPSVFLFPPKPKDTLMISRTPEVTCVVVDVSQEDPEVQFNWYVDGVEVHNAKTKPREEQFNSTYRVVSVLTVLHQDWLNGKEYKCKVSNKGLPSSIEKTISKAKGQPREPQVYTLPPSQEEMTKNQVSLTCLVKGFYPSDIAVEWESNGQPENNYKTTPPVLDSDGSFFLYSRLTVDKSRWQQGNVFSCSVMHEALHNHYTQKSLSLS"; +{ + const hit = findConstantRegion(PEMBROLIZUMAB_HC); + assert(hit && hit.isotype === "IGHG4" && hit.species === "human", + "pembrolizumab (5DK3): human IGHG4"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + assert(hit.aligned1[euCols.indexOf(228)] === "P", + "pembrolizumab: P228 (S228P hinge stabilization)"); + assert(CONST_REGION_DB.human.CH.IGHG4.s[getHumanIgGEuNumbers("IGHG4").indexOf(228)] === "S", + "wild-type IGHG4 reference is S228 (contrast with pembrolizumab)"); + assert(hit.aligned1[euCols.indexOf(234)] === "F", "pembrolizumab: F234"); + assert(hit.aligned1[euCols.indexOf(297)] === "N", "pembrolizumab: N297"); + // IgG4 hinge skips 221–223: Eu 220 then 224 + assert(hit.aligned1[euCols.indexOf(220)] === "G", "pembrolizumab: G220"); + assert(hit.aligned1[euCols.indexOf(224)] === "P", "pembrolizumab: P224"); + assert(euCols.indexOf(221) === -1, "pembrolizumab map has no Eu 221 column"); + const html = renderConstantRegionPanel(hit, "CH"); + assert(html.includes(">EU:"), "pembrolizumab panel includes EU row"); +} + +// Nivolumab IgG4 S228P — full HC from DDInter / DrugBank DB09035 sequence dump +// https://ddinter2.scbdd.com/server/drug-detail/DDInter1308/ +const NIVOLUMAB_HC = + "QVQLVESGGGVVQPGRSLRLDCKASGITFSNSGMHWVRQAPGKGLEWVAVIWYDGSKRYYADSVKGRFTISRDNSKNTLFLQMNSLRAEDTAVYYCATNDDYWGQGTLVTVSS" + + "ASTKGPSVFPLAPCSRSTSESTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSSLGTKTYTCNVDHKPSNTKVDKRVESKYGPPCPPCPAPEFLGGPSVFLFPPKPKDTLMISRTPEVTCVVVDVSQEDPEVQFNWYVDGVEVHNAKTKPREEQFNSTYRVVSVLTVLHQDWLNGKEYKCKVSNKGLPSSIEKTISKAKGQPREPQVYTLPPSQEEMTKNQVSLTCLVKGFYPSDIAVEWESNGQPENNYKTTPPVLDSDGSFFLYSRLTVDKSRWQEGNVFSCSVMHEALHNHYTQKSLSLSLGK"; +{ + const hit = findConstantRegion(NIVOLUMAB_HC); + assert(hit && hit.isotype === "IGHG4", "nivolumab: IGHG4"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + assert(hit.aligned1[euCols.indexOf(228)] === "P", "nivolumab: P228 (S228P)"); + assert(hit.aligned1[euCols.indexOf(297)] === "N", "nivolumab: N297"); + assert(hit.aligned1[euCols.indexOf(447)] === "K", "nivolumab: K447"); + // CH3 E419 (WQEGN) vs pembrolizumab Q419 (WQQGN) — both Eu 419 + assert(hit.aligned1[euCols.indexOf(419)] === "E", "nivolumab: E419 (WQEGN)"); +} + +// Panitumumab IgG2 Fab heavy (PDB 5SX4) — human IGHG2 CH1 + hinge start +// https://www.rcsb.org/structure/5SX4 +const PANITUMUMAB_FAB_HC = + "QVQLQESGPGLVKPSETLSLTCTVSGGSVSSGDYYWTWIRQSPGKGLEWIGHIYYSGNTNYNPSLKSRLTISIDTSKTQFSLKLSSVTAADTAIYYCVRDRVTGAFDIWGQGTMVTVSS" + + "ASTKGPSVFPLAPCSRSTSESTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSNFGTQTYTCNVDHKPSNTKVDKTVERKC"; +{ + assert( + PANITUMUMAB_FAB_HC.includes(HUMAN_IGHG2_CH_PREFIX()), + "panitumumab Fab CH matches IGHG2 through hinge Cys", + ); + const full = PANITUMUMAB_FAB_HC + CONST_REGION_DB.human.CH.IGHG2.s.slice(102); + const hit = findConstantRegion(full); + assert(hit && hit.isotype === "IGHG2", "panitumumab: IGHG2"); + const euCols = mapEuOntoConstantAlignment(hit.aligned2, hit.species, hit.isotype); + assert(hit.aligned1[euCols.indexOf(219)] === "C", "panitumumab: C219 (IgG2 hinge)"); + assert(hit.aligned1[euCols.indexOf(220)] === "C", "panitumumab: C220"); + assert(euCols.indexOf(221) === -1, "panitumumab IgG2: no Eu 221"); + assert(euCols.indexOf(233) === -1, "panitumumab IgG2: no Eu 233"); + assert(hit.aligned1[euCols.indexOf(297)] === "N", "panitumumab: N297"); + assert(hit.aligned1[euCols.indexOf(234)] === "P", "panitumumab: P234 (APPVAG)"); +} + +function HUMAN_IGHG2_CH_PREFIX() { + return CONST_REGION_DB.human.CH.IGHG2.s.slice(0, 102); +} + +// IgG1 Fc fragment PDB 4X4M — CH2–CH3 body matches IGHG1 from A231 +// https://www.rcsb.org/structure/4X4M (chain sequence begins CPAPELL…; +// residues after the leading C are identical to IGHG1 Eu 231–447). +const IGG1_FC_4X4M = + "CPAPELLGGPSVFLFPPKPKDTLMISRTPEVTCVVVDVSHEDPEVKFNWYVDGVEVHNAKTKPREEQYNSTYRVVSVLTVLHQDWLNGKEYKCKVSNKALPAPIEKTISKAKGQPREPQVYTLPPSRDELTKNQVSLTCLVKGFYPSDIAVEWESNGQPENNYKTTPPVLDSDGSFFLYSKLTVDKSRWQQGNVFSCSVMHEALHNHYTQKSLSLSPGK"; +{ + const g1 = CONST_REGION_DB.human.CH.IGHG1.s; + assert(g1.indexOf("APELLGG") === 113, "IGHG1 CH2 starts at index 113 (Eu 231)"); + // 4X4M is C229 + P230–K447 (common Fc crystal start at the last hinge Cys) + assert( + IGG1_FC_4X4M === "C" + g1.slice(112), + "4X4M Fc equals C229 + IGHG1 Eu 230–447", + ); + assert(aaAtEu("IGHG1", g1, 297) === "N", "4X4M/IGHG1 N297"); + assert(aaAtEu("IGHG1", g1, 329) === "P", "4X4M/IGHG1 P329"); +} + +// Isotype-specific hinge difference: same Eu 228 is S (IgG4 WT), P (IgG1), or absent numbering path +assert(aaAtEu("IGHG1", CONST_REGION_DB.human.CH.IGHG1.s, 228) === "P", "IGHG1 P228"); +assert(aaAtEu("IGHG4", CONST_REGION_DB.human.CH.IGHG4.s, 228) === "S", "IGHG4 S228"); +assert(aaAtEu("IGHG2", CONST_REGION_DB.human.CH.IGHG2.s, 228) === "P", "IGHG2 P228"); + // ============================================================ // alignJGene (semi-global: free prefix + free suffix, ref fully consumed) // Real IMGT J gene sequences and antibody variable regions throughout.