@@ -238,12 +238,41 @@ def get_rings(mol: Chem.Mol) -> dict[str, list]:
238238 return atom_extensions
239239
240240
241+ def _add_angular_methyls (
242+ mol : Chem .Mol ,
243+ atom_extensions : dict [str , list ],
244+ iupac_to_atom : dict [int , int ],
245+ ) -> None :
246+ """Label angular methyls C18 (on C13) and C19 (on C10) when present.
247+
248+ Looks for the unique carbon neighbour of the attachment atom that is not
249+ part of the gonane core (C1–C17). Estrogens without C19 simply yield no
250+ ``steroid_19`` predicate.
251+ """
252+ core_atom_indices = set (iupac_to_atom .values ())
253+ for attachment_position , methyl_position in ((13 , 18 ), (10 , 19 )):
254+ attachment_atom_idx = iupac_to_atom .get (attachment_position )
255+ if attachment_atom_idx is None :
256+ continue
257+ methyl_candidates = [
258+ neighbor .GetIdx ()
259+ for neighbor in mol .GetAtomWithIdx (attachment_atom_idx ).GetNeighbors ()
260+ if neighbor .GetAtomicNum () == 6 and neighbor .GetIdx () not in core_atom_indices
261+ ]
262+ if len (methyl_candidates ) == 1 :
263+ atom_extensions .setdefault (f"steroid_{ methyl_position } " , []).append (
264+ methyl_candidates [0 ]
265+ )
266+
267+
241268def get_steroid_positions (mol : Chem .Mol ) -> dict [str , list ]:
242269 """Extract steroid-nucleus position predicates.
243270
244271 Matches the molecule against the gonane core and, on a match, labels the
245272 ring atoms with their IUPAC steroid position as predicates ``steroid_1`` …
246- ``steroid_17``. Molecules without a gonane core yield no predicates.
273+ ``steroid_17``. When present, angular methyls are added as ``steroid_18``
274+ (on C13) and ``steroid_19`` (on C10). Molecules without a gonane core yield
275+ no predicates.
247276
248277 Parameters
249278 ----------
@@ -258,11 +287,17 @@ def get_steroid_positions(mol: Chem.Mol) -> dict[str, list]:
258287 """
259288 atom_extensions : dict [str , list ] = {}
260289 steroid_match = mol .GetSubstructMatch (_GONANE_PATTERN , useChirality = False )
261- if steroid_match :
262- for pat_idx , atom_idx in enumerate (steroid_match ):
263- iupac = _GONANE_IDX_TO_IUPAC .get (pat_idx )
264- if iupac is not None :
265- atom_extensions .setdefault (f"steroid_{ iupac } " , []).append (atom_idx )
290+ if not steroid_match :
291+ return atom_extensions
292+
293+ iupac_to_atom : dict [int , int ] = {}
294+ for pat_idx , atom_idx in enumerate (steroid_match ):
295+ iupac = _GONANE_IDX_TO_IUPAC .get (pat_idx )
296+ if iupac is not None :
297+ atom_extensions .setdefault (f"steroid_{ iupac } " , []).append (atom_idx )
298+ iupac_to_atom [iupac ] = atom_idx
299+
300+ _add_angular_methyls (mol , atom_extensions , iupac_to_atom )
266301 return atom_extensions
267302
268303
@@ -289,3 +324,25 @@ def get_numerical_facts(mol: Chem.Mol) -> dict[str, list]:
289324 for ring in mol .GetRingInfo ().AtomRings ():
290325 atom_extensions .setdefault ("ring_size" , []).append (len (ring ))
291326 return atom_extensions
327+
328+
329+ """Manual check for steroid numbering (not part of the library API).
330+
331+ Run: python -m chebi_utils.extract_properties
332+ Expect: cholesterol -> steroid_1..19, estrone -> steroid_1..18, benzene -> []
333+ """
334+ if __name__ == "__main__" :
335+ from chebi_utils .read_molecule import smiles_or_inchi_to_mol
336+
337+ for name , smiles in {
338+ "cholesterol" : (
339+ "C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C"
340+ ),
341+ "estrone" : "C[C@]12CC[C@H]3[C@H]([C@@H]1CCC2=O)CCc4c3ccc(O)c4" ,
342+ "benzene" : "c1ccccc1" ,
343+ }.items ():
344+ keys = sorted (
345+ get_steroid_positions (smiles_or_inchi_to_mol (smiles )),
346+ key = lambda k : int (k .split ("_" )[1 ]),
347+ )
348+ print (name , keys )
0 commit comments