Skip to content

Commit 1ea4630

Browse files
committed
[fix.stereo.λ.D8+] exclude aromatic bonds from E/Z unknowns
Kekulized aromatic rings were being audited as ordinary double bonds, so valid explicitly assigned chiral ligands were blocked by a fabricated UNSPECIFIED E/Z verdict. RDKit aromatic atom witnesses now exclude ring bonds while preserving exocyclic and ordinary stereogenic double bonds; a focused regression seals the boundary.
1 parent b34619c commit 1ea4630

3 files changed

Lines changed: 20 additions & 3 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { decideWorkbenchBoot } from './workbench-boot';
77
import { escapeHtml, markPipelineReadyDom, renderOperationConfirmationDom, renderPreparationControlsDom, renderPreparationPolicyDom, renderRunHistoryDom, renderRunJobsDom, setSafeText } from './workbench-dom';
88
import { acknowledgedPreparationReceipt, campaignScientificRefFrom, contentRef, fullJobId, operationBindingFromReceipt, preparationReceiptBinding, runReceiptFromData, runReceiptMatchesData, runReceiptState, type AcknowledgedPreparationReceipt, type AnyPreparationReceipt as PreparationReceipt, type ArtifactRef, type CampaignScientificRef, type ContentRef, type PreparationReceipt as DurablePreparationReceipt, type PlannerOutputReceipt, type PlannerReceipt, type RunReceipt, type RunReceiptState } from './workbench-receipts';
99
import { globalPhysicalReceiptKey, RECEIPT_KEYS, WorkbenchReceiptStore, type ReceiptStorage } from './workbench-receipt-store';
10-
import { OperationCoordinator, aggregateArmMatches, canonicalJson, chemistryEvidenceFrom, chemistryEvidenceView, exactOperationBindingMatches, executionEligibilityFrom, sameExactRef, type ExactAggregateArm, type ExactOperationBinding, type ExactRunBinding, type OperationScope } from './workbench-state';
10+
import { OperationCoordinator, aggregateArmMatches, canonicalJson, chemistryEvidenceFrom, chemistryEvidenceView, exactOperationBindingMatches, executionEligibilityFrom, isPotentialEzBond, sameExactRef, type ExactAggregateArm, type ExactOperationBinding, type ExactRunBinding, type OperationScope } from './workbench-state';
1111
import { aggregatePanelViewFrom, preparationPolicyGate, preparationPolicyViewFrom, runHistoryViewFrom, runJobsViewFrom } from './workbench-view-model';
1212
import { workbenchShellMarkup } from './workbench-shell';
1313
import { DemoCompounds, FallbackEdges, FallbackNetwork } from './workbench-fixture';
@@ -159,8 +159,10 @@ async function atomInfo(smiles: string): Promise<AtomInfo> {
159159
try{
160160
const stereo=JSON.parse(mol.get_stereo_tags()) as {CIP_atoms?:Array<[number,string]>;CIP_bonds?:Array<[number,number,string]>};cipAtoms=Array.isArray(stereo.CIP_atoms)?stereo.CIP_atoms:[];cipBonds=Array.isArray(stereo.CIP_bonds)?stereo.CIP_bonds:[];
161161
const json=JSON.parse(mol.get_json()) as {defaults?:{atom?:{impHs?:number}};molecules?:Array<{atoms?:Array<{impHs?:number}>;extensions?:Array<{name?:string;cipRanks?:number[]}>}>},molecule=json.molecules?.[0],atoms=molecule?.atoms||[],ranks=molecule?.extensions?.find(extension=>extension.name==='rdkitRepresentation')?.cipRanks||[],defaultHydrogens=Number(json.defaults?.atom?.impHs||0);
162+
const aromaticAtoms=new Set<number>();let aromaticQuery:ReturnType<typeof RDKit.get_qmol>|null=null;
163+
try{aromaticQuery=RDKit.get_qmol('[a]');const matches=aromaticQuery?JSON.parse(mol.get_substruct_matches(aromaticQuery)) as Array<{atoms?:number[]}>:[];if(Array.isArray(matches))matches.forEach(match=>match.atoms?.forEach(index=>aromaticAtoms.add(index)));}finally{aromaticQuery?.delete();}
162164
const substituentKeys=(atomIndex:number,partnerIndex:number):string[]=>{const keys=bonds.filter(bond=>bond.left===atomIndex||bond.right===atomIndex).map(bond=>bond.left===atomIndex?bond.right:bond.left).filter(index=>index!==partnerIndex).map(index=>`A:${String(ranks[index]??`${symbols[index]}:${index}`)}`),implicitHydrogens=Number(atoms[atomIndex]?.impHs??defaultHydrogens);for(let index=0;index<implicitHydrogens;index++)keys.push('H');return keys;};
163-
potentialEzBonds=bonds.filter(bond=>bond.order===2&&!cipBonds.some(([left,right])=>(left===bond.left&&right===bond.right)||(left===bond.right&&right===bond.left))).filter(bond=>{const left=substituentKeys(bond.left,bond.right),right=substituentKeys(bond.right,bond.left);return left.length===2&&right.length===2&&left[0]!==left[1]&&right[0]!==right[1];}).map(bond=>[bond.left,bond.right]);
165+
potentialEzBonds=bonds.filter(bond=>isPotentialEzBond(bond,aromaticAtoms)&&!cipBonds.some(([left,right])=>(left===bond.left&&right===bond.right)||(left===bond.right&&right===bond.left))).filter(bond=>{const left=substituentKeys(bond.left,bond.right),right=substituentKeys(bond.right,bond.left);return left.length===2&&right.length===2&&left[0]!==left[1]&&right[0]!==right[1];}).map(bond=>[bond.left,bond.right]);
164166
}catch{stereoAvailable=false;}
165167
const result = { symbols, molblock, canonicalSmiles, charges, atomStereo, bonds, cycleRank, stereoAvailable, cipAtoms, cipBonds, potentialEzBonds }; atomInfoCache.set(smiles, result); return result;
166168
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { describe, expect, it } from '@jest/globals';
2-
import { CHEMISTRY_DIMENSIONS, OperationCoordinator, aggregateArmMatches, campaignsVisibleToCopy, canonicalJson, chemistryEvidenceFrom, chemistryEvidenceView, exactOperationBindingMatches, exactRunBindingMatches, executionEligibilityFrom, sameExactRef, type ChemistryDimension, type ChemistryVerdict, type ExactOperationBinding, type ExactRunBinding } from './workbench-state';
2+
import { CHEMISTRY_DIMENSIONS, OperationCoordinator, aggregateArmMatches, campaignsVisibleToCopy, canonicalJson, chemistryEvidenceFrom, chemistryEvidenceView, exactOperationBindingMatches, exactRunBindingMatches, executionEligibilityFrom, isPotentialEzBond, sameExactRef, type ChemistryDimension, type ChemistryVerdict, type ExactOperationBinding, type ExactRunBinding } from './workbench-state';
33

44
describe('FEP workbench operation ownership', () => {
5+
it('excludes kekulized aromatic bonds from unknown E/Z candidates', () => {
6+
const aromaticAtoms = new Set([0, 1, 2, 3, 4, 5]);
7+
expect(isPotentialEzBond({ left: 0, right: 1, order: 2 }, aromaticAtoms)).toBe(false);
8+
expect(isPotentialEzBond({ left: 5, right: 6, order: 2 }, aromaticAtoms)).toBe(true);
9+
expect(isPotentialEzBond({ left: 6, right: 7, order: 1 }, aromaticAtoms)).toBe(false);
10+
});
11+
512
it('invalidates competitors while allowing the transition owner to continue', () => {
613
const coordinator = new OperationCoordinator('tab-a');
714
const owner = coordinator.begin('restore');

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,11 @@ export function chemistryEvidenceView(value: unknown): ChemistryEvidenceView {
317317
ledger,
318318
};
319319
}
320+
321+
/** Kekulized mol blocks encode aromatic rings as alternating 1/2 bonds. */
322+
export function isPotentialEzBond(
323+
bond: { left: number; right: number; order: number },
324+
aromaticAtoms: ReadonlySet<number>,
325+
): boolean {
326+
return bond.order === 2 && !(aromaticAtoms.has(bond.left) && aromaticAtoms.has(bond.right));
327+
}

0 commit comments

Comments
 (0)