Skip to content

Commit 6c13b8f

Browse files
authored
web app (#39)
web app
1 parent f117a1a commit 6c13b8f

38 files changed

Lines changed: 1976 additions & 31 deletions

src/openclatura/assembly_parts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,5 @@ class AssemblyParts:
130130
name_token_spans: list[dict] = field(default_factory=list)
131131
name_rewrite_history: list[dict] = field(default_factory=list)
132132
stereo_audit_issues: list[str] = field(default_factory=list)
133+
reconstruction_audit_status: str | None = None
134+
reconstruction_audit_issues: list[str] = field(default_factory=list)

src/openclatura/component_namer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
filter_component_groups_to_parent,
2727
partition_principal_and_prefix_groups,
2828
)
29+
from .reconstruction_audit import audit_component_reconstruction
2930
from .retained_fused_production import production_retained_fused_parent
3031
from .special_cases import (
3132
single_atom_component_name,
@@ -611,6 +612,9 @@ def name_component_again(next_mol: Molecule, next_atoms: set[int], is_substituen
611612

612613
refresh_name_atom_bindings(parts)
613614
parts.stereo_audit_issues = list(audit_stereochemistry(mol, parts).issues)
615+
reconstruction = audit_component_reconstruction(mol, parts)
616+
parts.reconstruction_audit_status = reconstruction.status
617+
parts.reconstruction_audit_issues = list(reconstruction.issues)
614618
assert_component_fully_named(mol, state.component_atoms, parts, "<component>")
615619
name = assemble_parent_name(mol, parts, numbered_path, get_loc, emit_metadata=emit_metadata)
616620
if emit_metadata:
@@ -629,6 +633,10 @@ def name_component_again(next_mol: Molecule, next_atoms: set[int], is_substituen
629633
"substituent_count": len(parts.substituents),
630634
"unsaturation_count": len(parts.unsaturations),
631635
"stereo_audit_issues": parts.stereo_audit_issues,
636+
"reconstruction_audit": {
637+
"status": parts.reconstruction_audit_status,
638+
"issues": parts.reconstruction_audit_issues,
639+
},
632640
"name_atom_bindings": binding_trace_data(parts.name_atom_bindings, include_emitted_tokens=token_debug),
633641
"name_token_spans": parts.name_token_spans if token_debug else [],
634642
"name_rewrite_history": parts.name_rewrite_history,

src/openclatura/graph_io.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""SMILES input and graph component helpers."""
22

33
from rdkit import Chem
4+
from rdkit.Chem import rdCIPLabeler
45

56
from .molecule import Molecule
67

@@ -29,31 +30,53 @@ def read_smiles(smiles: str) -> Molecule:
2930
atom_metadata = _atom_metadata(rdmol)
3031

3132
Chem.AssignStereochemistry(rdmol, force=True, cleanIt=True)
32-
chiral_centers = dict(Chem.FindMolChiralCenters(rdmol, includeUnassigned=False))
33+
legacy_centers = dict(Chem.FindMolChiralCenters(rdmol, includeUnassigned=False))
34+
# The legacy labeler decides *which* centers carry an absolute descriptor:
35+
# centers it leaves unassigned are ring-symmetry-dependent ones that the
36+
# scoped small-ring / cis-trans fallbacks must handle (OPSIN rejects
37+
# absolute R/S there). But its CIP *values* mis-rank ligands in
38+
# deep-sphere comparisons (e.g. ring-closure duplicate atoms) and invert
39+
# 3-coordinate sulfur centers, so the label itself comes from
40+
# rdCIPLabeler, which overwrites the _CIPCode properties.
41+
try:
42+
rdCIPLabeler.AssignCIPLabels(rdmol)
43+
except Exception:
44+
pass # unsanitized input; keep the legacy labels
45+
chiral_centers = {}
46+
for idx, legacy_code in legacy_centers.items():
47+
atom = rdmol.GetAtomWithIdx(idx)
48+
code = atom.GetProp("_CIPCode") if atom.HasProp("_CIPCode") else legacy_code
49+
if code in ("R", "S"):
50+
chiral_centers[idx] = code
3351

3452
for atom in rdmol.GetAtoms():
3553
stereo = chiral_centers.get(atom.GetIdx())
36-
if stereo and atom.GetSymbol() == "S" and atom.GetTotalDegree() == 3:
37-
stereo = "R" if stereo == "S" else "S"
3854
raw_stereo = _raw_tetrahedral_stereo(atom) if not stereo else None
3955
mol.add_atom(
4056
symbol=atom.GetSymbol(),
4157
idx=atom.GetIdx(),
4258
charge=atom.GetFormalCharge(),
4359
stereo=stereo,
4460
raw_stereo=raw_stereo,
61+
cip_code=atom.GetProp("_CIPCode") if atom.HasProp("_CIPCode") else None,
4562
is_aromatic=atom_metadata[atom.GetIdx()]["is_aromatic"],
4663
explicit_h_count=atom_metadata[atom.GetIdx()]["explicit_h_count"],
4764
total_h_count=atom_metadata[atom.GetIdx()]["total_h_count"],
4865
)
4966

5067
for bond in rdmol.GetBonds():
51-
stereo = None
52-
st = bond.GetStereo()
53-
if st == Chem.rdchem.BondStereo.STEREOE:
54-
stereo = "E"
55-
elif st == Chem.rdchem.BondStereo.STEREOZ:
56-
stereo = "Z"
68+
# rdCIPLabeler rewrites bond enums to STEREOCIS/STEREOTRANS but stamps
69+
# the authoritative E/Z label as _CIPCode; fall back to the enums when
70+
# the labeler did not run.
71+
stereo = bond.GetPropsAsDict().get("_CIPCode")
72+
if stereo not in ("E", "Z"):
73+
st = bond.GetStereo()
74+
if st == Chem.rdchem.BondStereo.STEREOE:
75+
stereo = "E"
76+
elif st == Chem.rdchem.BondStereo.STEREOZ:
77+
stereo = "Z"
78+
else:
79+
stereo = None
5780

5881
in_small_ring = any(bond.IsInRingSize(i) for i in range(3, 8))
5982

src/openclatura/molecule.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Atom:
1313
isotope: int | None = None
1414
stereo: str | None = None # 'R' or 'S'
1515
raw_stereo: str | None = None # RDKit tetrahedral tag when CIP is unavailable: 'CW' or 'CCW'
16+
cip_code: str | None = None # full-molecule rdCIPLabeler code ('R'/'S'/'r'/'s'), also set on dependent centers
1617
is_aromatic: bool = False
1718
explicit_h_count: int = 0
1819
total_h_count: int = 0
@@ -179,6 +180,7 @@ def add_atom(
179180
stereo: str | None = None,
180181
*,
181182
raw_stereo: str | None = None,
183+
cip_code: str | None = None,
182184
is_aromatic: bool = False,
183185
explicit_h_count: int = 0,
184186
total_h_count: int = 0,
@@ -193,6 +195,7 @@ def add_atom(
193195
charge=charge,
194196
stereo=stereo,
195197
raw_stereo=raw_stereo,
198+
cip_code=cip_code,
196199
is_aromatic=is_aromatic,
197200
explicit_h_count=explicit_h_count,
198201
total_h_count=total_h_count,

0 commit comments

Comments
 (0)