Skip to content

Commit 06b2926

Browse files
committed
[act.fep.ρ.D8+] turn a protein name into a usable receptor
The FEP builder still assumed that a new user already knew which PDB accession and crystallographic ligand to choose. That made the first screen an expert-only database form instead of a target-design workflow. Add an official RCSB experimental-structure search with target-aware ranking and visible reasons, then reuse the existing coordinate inspection path when a candidate is chosen. Treat common nucleotide analogues and cofactors separately from drug-like ligands; when exactly one drug-like bound ligand remains, select it visibly so the next task is adding compounds rather than deciphering HETATM records. The recommendation stays reviewable: no structure is loaded until the user clicks it, every rank exposes target match, bound components, method, resolution, and modeled-residue coverage, and direct PDB/upload/database entry remains available. Verified with 99 FEP tests, strict TypeScript, focused ESLint, both production builds, architecture-twin parity, and an isolated Chromium EGFR flow that searched six candidates, loaded 5D41, excluded ANP, selected 57N, and unlocked Add Compounds without submitting a preparation or FEP job.
1 parent 93d07ed commit 06b2926

8 files changed

Lines changed: 5369 additions & 4066 deletions

File tree

‎docs/architecture/dirac-digital-twin.html‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎docs/architecture/dirac-digital-twin.json‎

Lines changed: 5181 additions & 4035 deletions
Large diffs are not rendered by default.

‎src/app.frontend.facets.molstar-rdkit.editable/fep-workbench/fep-workbench.css‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/app.frontend.facets.molstar-rdkit.editable/fep-workbench/fep-workbench.ts‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { benchmarkEdgeResult,benchmarkResult } from './workbench-benchmark-resul
1515
import { renderResultShowcase } from './workbench-result-showcase';
1616
import { CAMPAIGN_CACHE_KEYS, createCampaignState, draftFromCampaignEnvelope } from './workbench-campaign-state';
1717
import { createStoredPreparationReceipt, exactPreparationResultFrom, preparationElapsedSeconds, preparationReceiptMatchesOpenCampaign, preparationResultMatchesOpenCampaign, preparationSubmissionLockName, preparedSystemFromPreparationResult, submitPreparationExactlyOnce } from './workbench-preparation';
18-
import { applyGuidedExample,builderReadinessCopy,campaignEstimate,clearGuidedCampaignForm,fetchPdbExperimentalRecord,ligandIdentityCardHtml,renderBuilderGuide,renderBuilderGuideProgress,renderCampaignEstimate,renderDecisionValidation,renderLigandAuditRows,renderParentCompoundOptions,restoreParentCompoundSelection,T4L_EIGHT_LIGAND_EXAMPLE,validateDecisionInputs,type BuilderGuideStep,type BuilderUxMode } from './workbench-guided-build';
18+
import { applyGuidedExample,builderReadinessCopy,campaignEstimate,clearGuidedCampaignForm,ligandIdentityCardHtml,renderBuilderGuide,renderBuilderGuideProgress,renderCampaignEstimate,renderDecisionValidation,renderLigandAuditRows,renderParentCompoundOptions,restoreParentCompoundSelection,T4L_EIGHT_LIGAND_EXAMPLE,validateDecisionInputs,type BuilderGuideStep,type BuilderUxMode } from './workbench-guided-build';
19+
import { bindTargetStructureControls,fetchPdbExperimentalRecord,inspectBoundLigands,type BoundLigandCandidate } from './workbench-target-search';
1920
import type { AtomInfo, Bond, BuilderStage, CampaignDraftV2, Compound, DepictionContract, Edge, ExecutionContract, PreparedSystemOption, RunJob, Network } from './workbench-types';
2021
const query = new URLSearchParams(location.search);
2122
const resultShowcase=query.get('showcase')==='results';
@@ -584,7 +585,6 @@ let builderNoticeTimer=0;
584585
let builderReturnFocus:HTMLElement|null=null;
585586
let receptorInputReady=false;
586587
let receptorSourceLabel='NO RECEPTOR';
587-
type BoundLigandCandidate={resname:string;chain:string;residue_number:string;heavy_atom_count:number;label:string;role:'ligand'|'cofactor'};
588588
type LigandInputRow={id:string;smiles:string;sourceLine:number;raw:string;parseError?:string};
589589
let builderStage:BuilderStage='inputs';
590590
let receptorPdbText='';
@@ -604,13 +604,6 @@ let builderUxMode:BuilderUxMode='guided';
604604
function reflectBuilderStage():void { campaignBuilder?.setAttribute('data-stage',builderStage); }
605605
function currentLigandSignature():string { return ((document.getElementById('campaign-ligands') as HTMLTextAreaElement|null)?.value||'').replace(/\r\n/g,'\n').trim(); }
606606

607-
function inspectBoundLigands(pdbText:string):BoundLigandCandidate[] {
608-
const excluded=new Set(['HOH','WAT','DOD','NA','CL','K','CA','MG','MN','ZN','FE','CU','CO','NI','CD','HG','BR','IOD','SO4','PO4','GOL','EDO']);
609-
const cofactors=new Set(['HEM','HEC','FAD','FMN','NAD','NAP','SAM','SAH','COA','PLP','TPP']);
610-
const groups=new Map<string,{resname:string;chain:string;residue_number:string;atoms:number}>();
611-
pdbText.split(/\r?\n/).forEach(line=>{ if (!line.startsWith('HETATM')||line.length<54) return; const resname=line.slice(17,20).trim().toUpperCase(),chain=line.slice(21,22).trim(),residue_number=line.slice(22,27).trim(),element=(line.length>=78?line.slice(76,78):line.slice(12,14)).trim().toUpperCase(); if (excluded.has(resname)||element==='H'||element==='D') return; const key=`${resname}|${chain}|${residue_number}`,row=groups.get(key)||{ resname,chain,residue_number,atoms: 0 }; row.atoms++; groups.set(key,row); });
612-
return [...groups.values()].sort((a,b)=>Number(cofactors.has(a.resname))-Number(cofactors.has(b.resname))||b.atoms-a.atoms||a.resname.localeCompare(b.resname)).map(row=>({ ...row,heavy_atom_count: row.atoms,role: cofactors.has(row.resname)?'cofactor':'ligand',label: `${cofactors.has(row.resname)?'COFACTOR · ':''}${row.resname} · CHAIN ${row.chain||'—'} · RES ${row.residue_number} · ${row.atoms} HEAVY ATOMS` }));
613-
}
614607
function selectedReferenceLigand():BoundLigandCandidate|null {
615608
const value=(document.getElementById('site-select') as HTMLSelectElement|null)?.value||''; return value===''?null:boundLigands[Number(value)]||null;
616609
}
@@ -623,7 +616,7 @@ function populateBoundLigands():void {
623616
boundLigands=inspectBoundLigands(receptorPdbText); const select=document.getElementById('site-select') as HTMLSelectElement|null; if (!select) return;
624617
const role=(document.getElementById('site-role-filter') as HTMLSelectElement|null)?.value||'ligand';
625618
const visible=boundLigands.map((row,index)=>({ row,index })).filter(({ row })=>role==='all'||row.role==='ligand');
626-
select.disabled=!visible.length; select.innerHTML=visible.length?'<option value="">SELECT THE BOUND CRYSTAL LIGAND…</option>'+visible.map(({ row,index },position)=>`<option value="${index}">${position===0?'START HERE · VERIFY IDENTITY · ':''}${escapeHtml(row.label)}</option>`).join(''):'<option value="">NO DRUG-LIKE BOUND LIGAND FOUND</option>';
619+
select.disabled=!visible.length; select.innerHTML=visible.length?'<option value="">SELECT THE BOUND CRYSTAL LIGAND…</option>'+visible.map(({ row,index },position)=>`<option value="${index}">${position===0?'START HERE · VERIFY IDENTITY · ':''}${escapeHtml(row.label)}</option>`).join(''):'<option value="">NO DRUG-LIKE BOUND LIGAND FOUND</option>'; if (role==='ligand'&&visible.length===1)select.value=String(visible[0].index);
627620
const selected=selectedReferenceLigand(); text('pose-reference-state',selected?selected.label:'NO REFERENCE · ALIGNMENT UNAVAILABLE');
628621
}
629622
function resetBuilderProgress():void {
@@ -795,12 +788,6 @@ document.getElementById('builder-build')?.addEventListener('click',()=>{ setMain
795788
document.getElementById('close-builder')?.addEventListener('click',()=>{ setMainMode('review'); setBuilderOpen(false); });
796789
document.getElementById('builder-review')?.addEventListener('click',()=>{ setMainMode('review'); setBuilderOpen(false); renderRunJobs(); });
797790
document.getElementById('builder-runs')?.addEventListener('click',showRunsWorkspace);
798-
document.querySelectorAll<HTMLButtonElement>('[data-target-source]').forEach(button=>button.addEventListener('click',()=>{
799-
document.querySelectorAll<HTMLButtonElement>('[data-target-source]').forEach(item=>{ item.classList.toggle('active',item===button); item.setAttribute('aria-pressed',String(item===button)); });
800-
const mode=button.dataset.targetSource;
801-
(['pdb','upload','existing'] as const).forEach(source=>{ const panel=document.getElementById(`source-${source}-panel`); if (panel)panel.hidden=source!==mode; });
802-
showBuilderNotice(mode==='pdb'?'Enter a PDB accession; Dirac will retrieve and inspect the experimental structure.':mode==='upload'?'Upload fixed-column PDB coordinates. mmCIF is not accepted in this release. Protein preparation remains a generated backend job, not a prerequisite.':'Reuse a versioned receptor only when it is scientifically the same system.');
803-
}));
804791
document.querySelectorAll<HTMLButtonElement>('[data-choice-group] button').forEach(button=>button.addEventListener('click',()=>selectChoice(button)));
805792
document.querySelectorAll<HTMLButtonElement>('[data-advanced]').forEach(button=>{
806793
button.setAttribute('aria-expanded','false');
@@ -820,9 +807,10 @@ async function inspectPdb(pdb:string,preferredReference=''):Promise<boolean> {
820807
if (!invalidateScientificState('receptor',`PDB ${pdb} loaded`)) return false;
821808
const normalized=method.toUpperCase(); receptorPdbText=downloadedPdb; receptorRecord={ title,method: normalized.includes('X-RAY')?'xray':normalized.includes('ELECTRON')?'cryoem':normalized.includes('NMR')?'nmr':'model',resolution: Number.isFinite(resolution)?Number(resolution):null }; receptorInputReady=true; receptorSourceLabel=pdb; resetBuilderProgress(); populateBoundLigands();
822809
if (preferredReference) { const index=boundLigands.findIndex(row=>row.resname===preferredReference); const select=document.getElementById('site-select') as HTMLSelectElement|null; if (select&&index>=0)select.value=String(index); }
823-
text('pose-reference-state',selectedReferenceLigand()?.label||'NO REFERENCE · ALIGNMENT UNAVAILABLE'); text('receptor-preview-title',`${pdb} · ${title}`); text('receptor-preview-detail',`${method}${Number.isFinite(resolution)?` · ${Number(resolution).toFixed(2)} Å`:''} · ${boundLigands.length} bound organic candidate${boundLigands.length===1?'':'s'} found`); updateBuilderReadiness(); showBuilderNotice(preferredReference&&selectedReferenceLigand()?`${pdb} loaded · ${preferredReference} selected as the bound reference.`:`${pdb} coordinates loaded. Choose the bound ligand that corresponds to your reference compound.`); return true;
810+
const reference=selectedReferenceLigand(); text('pose-reference-state',reference?.label||'NO REFERENCE · ALIGNMENT UNAVAILABLE'); text('receptor-preview-title',`${pdb} · ${title}`); text('receptor-preview-detail',`${method}${Number.isFinite(resolution)?` · ${Number(resolution).toFixed(2)} Å`:''} · ${boundLigands.length} bound organic candidate${boundLigands.length===1?'':'s'} found`); updateBuilderReadiness(); showBuilderNotice(reference?`${pdb} loaded · ${reference.resname} selected as the drug-like bound reference · verify identity before preparation.`:`${pdb} coordinates loaded. Choose the bound ligand that corresponds to your reference compound.`); return true;
824811
} catch { if (requestId!==pdbFetchGeneration||epoch!==draftEpoch) return false; text('receptor-preview-title',`${pdb} · STRUCTURE NOT FOUND`); text('receptor-preview-detail',receptorPdbText?'Existing receptor was preserved; the failed response was not applied.':'No structure was loaded. Check the PDB accession at RCSB, or upload a PDB file.'); updateBuilderReadiness(); showBuilderNotice(`RCSB could not load ${pdb}. Check the accession or upload the structure file.`); return false; } finally { if (requestId===pdbFetchGeneration) { button.disabled=false; button.textContent='FETCH & INSPECT'; } }
825812
}
813+
bindTargetStructureControls(document,{ loadStructure: pdb=>inspectPdb(pdb),locked: runContextLocked,notify: showBuilderNotice });
826814
document.getElementById('inspect-pdb')?.addEventListener('click',async()=>{
827815
const pdb=(document.getElementById('campaign-pdb') as HTMLInputElement|null)?.value.trim().toUpperCase()||'';
828816
await inspectPdb(pdb);

‎src/app.frontend.facets.molstar-rdkit.editable/fep-workbench/workbench-guided-build.ts‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,3 @@ export function clearGuidedCampaignForm(document:Document):void {
174174
for (const id of ['campaign-question','assay-anchor','selectivity-context','adme-context','compound-priorities','pose-hypothesis','cost-cap','next-action','stop-rule']) { const element=document.getElementById(id) as HTMLInputElement|HTMLTextAreaElement|null; if (element)element.value=''; }
175175
for (const id of ['synthesis-status','portfolio-priority']) { const element=document.getElementById(id) as HTMLSelectElement|null; if (element)element.value=''; }
176176
}
177-
178-
export async function fetchPdbExperimentalRecord(pdb:string):Promise<{record:Record<string,any>;coordinates:string}> {
179-
const [metadata,coordinates]=await Promise.all([
180-
fetch(`https://data.rcsb.org/rest/v1/core/entry/${encodeURIComponent(pdb)}`),
181-
fetch(`https://files.rcsb.org/download/${encodeURIComponent(pdb)}.pdb`),
182-
]);
183-
if (!metadata.ok) throw new Error(`RCSB metadata returned HTTP ${metadata.status}`);
184-
if (!coordinates.ok) throw new Error(`RCSB coordinates returned HTTP ${coordinates.status}`);
185-
const record=await metadata.json() as Record<string,any>,pdbText=await coordinates.text();
186-
if (!pdbText.includes('\nATOM ')&&!pdbText.startsWith('ATOM ')) throw new Error('downloaded record contains no PDB ATOM coordinates');
187-
return { record,coordinates: pdbText };
188-
}

‎src/app.frontend.facets.molstar-rdkit.editable/fep-workbench/workbench-shell.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function workbenchShellMarkup(): string {
7676
<section class="build-card target-card">
7777
<div class="build-card-head"><span><small>01</small><b>TARGET &amp; RECEPTOR</b></span><em>REQUIRED</em></div>
7878
<div class="source-tabs" role="group" aria-label="Receptor source"><button id="source-tab-pdb" class="active" aria-pressed="true" data-target-source="pdb">PDB ID</button><button id="source-tab-upload" aria-pressed="false" data-target-source="upload">UPLOAD STRUCTURE</button><button id="source-tab-existing" aria-pressed="false" data-target-source="existing">DATABASE</button></div>
79-
<div id="source-pdb-panel" class="source-panel"><label class="hero-input"><span>STRUCTURE ACCESSION</span><div><input id="campaign-pdb" value="" placeholder="EXAMPLE: 181L" autocomplete="off" spellcheck="false"><button id="inspect-pdb">FETCH &amp; INSPECT</button></div></label><p class="source-help">A PDB ID identifies an experimental protein structure. If you only know the protein name, find a suitable ligand-bound entry in RCSB PDB first; the ready-made example requires no search.</p></div>
79+
<div id="source-pdb-panel" class="source-panel"><section class="protein-search"><label class="hero-input"><span>WHAT PROTEIN ARE YOU WORKING ON?</span><div><input id="protein-name-search" value="" placeholder="EGFR, CDK2, KRAS G12D…" autocomplete="off" spellcheck="false"><button id="search-protein-structures">FIND STRUCTURES</button></div></label><p class="source-help">We rank experimental structures by target match, bound components, method, resolution and modeled residues. You choose before anything is loaded.</p><div id="protein-search-status" class="protein-search-status" role="status" aria-live="polite">ENTER A PROTEIN NAME OR GENE SYMBOL</div><div id="protein-search-results" class="protein-search-results" hidden></div></section><div class="source-divider"><span>OR ENTER A PDB ID</span></div><label class="hero-input accession-input"><span>STRUCTURE ACCESSION</span><div><input id="campaign-pdb" value="" placeholder="EXAMPLE: 181L" autocomplete="off" spellcheck="false"><button id="inspect-pdb">FETCH &amp; INSPECT</button></div></label></div>
8080
<div id="source-upload-panel" class="source-panel" hidden><label class="source-drop"><input id="receptor-file" type="file" accept=".pdb,.ent"><i>+</i><span><b>DROP RAW PDB</b><small>Dirac prepares the protein after upload; do not pre-build an OpenFE system.</small></span><em>SELECT FILE</em></label></div>
8181
<div id="source-existing-panel" class="source-panel" hidden><label class="hero-input"><span>VERSIONED RECEPTOR ASSET</span><div><select id="builder-system-select"><option value="">NO REGISTERED SYSTEM SELECTED</option></select><button id="inspect-existing">INSPECT PROVENANCE</button></div></label></div>
8282
<div class="receptor-preview">

0 commit comments

Comments
 (0)