File size: 1,959 Bytes
05cb1f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit.Chem import Descriptors3D


def extract_3d_shape_features(smiles: str) -> list[float]:
    """
    Generates a 3D conformation for a SMILES string and extracts geometric shape
    and chirality descriptors to capture stereochemical variations.

    Returns a 7-D vector:
        [Asphericity, Eccentricity, InertialShapeFactor, SpherocityIndex,
         RadiusOfGyration, PBF, ChiralScore]
    """
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return [0.0] * 7

    # Step 1: Add hydrogens for accurate physical volume and surface estimations.
    mol = Chem.AddHs(mol)

    # Step 2: Generate 3D conformation using ETKDG v3.
    params = AllChem.ETKDGv3()
    params.randomSeed = 42
    conf_id = AllChem.EmbedMolecule(mol, params)

    if conf_id < 0:
        # Fallback to empty features if 3D coordinate embedding fails.
        return [0.0] * 7

    # Step 3: Optimize geometry using MMFF94 force field.
    try:
        AllChem.MMFFOptimizeMolecule(mol, confId=conf_id)
    except Exception:
        pass  # Keep raw embedded coordinates if minimization fails.

    # Step 4: Extract 3D shape descriptors.
    desc_3d = Descriptors3D.CalcMolDescriptors3D(mol, confId=conf_id)

    feature_vector = [
        float(desc_3d.get("Asphericity", 0.0)),
        float(desc_3d.get("Eccentricity", 0.0)),
        float(desc_3d.get("InertialShapeFactor", 0.0)),
        float(desc_3d.get("SpherocityIndex", 0.0)),
        float(desc_3d.get("RadiusOfGyration", 0.0)),
        float(desc_3d.get("PBF", 0.0)),  # Plane of best fit.
    ]

    # Step 5: Score chiral centers to differentiate mirror reflections.
    chiral_centers = Chem.FindMolChiralCenters(mol, includeUnassigned=True)
    chiral_score = sum(
        1.0 if center[1] == "R" else -1.0 for center in chiral_centers
    )
    feature_vector.append(float(chiral_score))

    return feature_vector