77from .nomenclature import RULES
88from .principal_suffixes import render_principal_suffix
99from .rules import bonds , stems
10+ from .stereo_descriptors import ABSOLUTE_STEREO_DESCRIPTORS , BOND_STEREO_DESCRIPTORS
1011
1112
1213def refresh_name_atom_bindings (parts : AssemblyParts ) -> list [NameAtomBinding ]:
1314 """Populate structured bindings for the current assembly parts."""
1415
16+ preserved_assembly_stereo = [
17+ binding
18+ for binding in parts .name_atom_bindings
19+ if binding .stage == "assembly" and binding .role in {"relative_stereo" , "small_ring_stereo" }
20+ ]
1521 bindings : list [NameAtomBinding ] = []
1622 if parts .parent_atom_ids :
1723 bindings .append (
@@ -37,6 +43,40 @@ def refresh_name_atom_bindings(parts: AssemblyParts) -> list[NameAtomBinding]:
3743 emitted_tokens = _hydro_operation_tokens (operation ),
3844 )
3945 )
46+ for locant , descriptor in parts .stereo_features :
47+ if descriptor not in ABSOLUTE_STEREO_DESCRIPTORS or not locant :
48+ continue
49+ atom_idx = parts .parent_atom_ids_by_locant .get (str (locant ))
50+ if atom_idx is None :
51+ continue
52+ bindings .append (
53+ NameAtomBinding (
54+ stage = "assembly" ,
55+ role = "absolute_stereo" ,
56+ term = f"{ locant } { descriptor } " ,
57+ atom_ids = {atom_idx },
58+ locants = (str (locant ),),
59+ emitted_tokens = _absolute_stereo_tokens (str (locant ), descriptor , atom_idx ),
60+ )
61+ )
62+ for locant , descriptor in parts .stereo_features :
63+ if descriptor not in BOND_STEREO_DESCRIPTORS or not locant :
64+ continue
65+ _bond_locants , atom_ids , bond_ids = _bond_stereo_graph_scope (parts , str (locant ))
66+ if not atom_ids :
67+ continue
68+ bindings .append (
69+ NameAtomBinding (
70+ stage = "assembly" ,
71+ role = "bond_stereo" ,
72+ term = descriptor ,
73+ atom_ids = atom_ids ,
74+ bond_ids = bond_ids ,
75+ locants = (str (locant ),),
76+ emitted_tokens = _bond_stereo_tokens (str (locant ), descriptor , atom_ids , bond_ids ),
77+ )
78+ )
79+ bindings .extend (preserved_assembly_stereo )
4080 if parts .front_modifiers :
4181 bindings .append (
4282 NameAtomBinding (
@@ -328,6 +368,8 @@ def _operation_emitted_tokens(binding: NameAtomBinding) -> tuple[NameTokenBindin
328368 return _locanted_operation_tokens (binding , charge_from_binding = True )
329369 if binding .stage == "charge" or binding .role == "parent_charge" :
330370 return _charge_operation_tokens (binding )
371+ if binding .stage == "assembly" and binding .role == "relative_stereo" :
372+ return _relative_stereo_tokens (binding )
331373 if binding .stage == "assembly" and "stereo" in binding .role :
332374 return _locanted_operation_tokens (binding )
333375 if binding .stage == "modifier" :
@@ -430,6 +472,114 @@ def _hydro_operation_tokens(operation) -> tuple[NameTokenBinding, ...]:
430472 return tuple (tokens )
431473
432474
475+ def _absolute_stereo_tokens (locant : str , descriptor : str , atom_idx : int ) -> tuple [NameTokenBinding , ...]:
476+ """Return native tokens for an absolute stereochemical descriptor."""
477+
478+ return (
479+ NameTokenBinding (
480+ text = locant ,
481+ token_kind = "locant" ,
482+ ownership = "exact" ,
483+ confidence = "exact" ,
484+ source = "renderer_stereo" ,
485+ grammar_role = "absolute_stereo" ,
486+ binding_key = "assembly:absolute_stereo" ,
487+ atom_ids = {atom_idx },
488+ locants = (locant ,),
489+ ),
490+ NameTokenBinding (
491+ text = descriptor ,
492+ token_kind = "stereo" ,
493+ ownership = "exact" ,
494+ confidence = "exact" ,
495+ source = "renderer_stereo" ,
496+ grammar_role = "absolute_stereo" ,
497+ binding_key = "assembly:absolute_stereo" ,
498+ atom_ids = {atom_idx },
499+ locants = (locant ,),
500+ ),
501+ )
502+
503+
504+ def _bond_stereo_graph_scope (parts : AssemblyParts , locant : str ) -> tuple [tuple [str , ...], set [int ], set [int ]]:
505+ """Return the graph scope for an E/Z descriptor rendered at a parent locant."""
506+
507+ for locant_pair , order in parts .parent_bond_orders_by_locants .items ():
508+ if locant not in locant_pair or order != 2 :
509+ continue
510+ atom_ids = {
511+ atom_idx
512+ for pair_locant in locant_pair
513+ if (atom_idx := parts .parent_atom_ids_by_locant .get (str (pair_locant ))) is not None
514+ }
515+ if len (atom_ids ) != 2 :
516+ continue
517+ bond_id = parts .parent_bond_ids_by_locants .get (locant_pair )
518+ return (
519+ tuple (str (pair_locant ) for pair_locant in locant_pair ),
520+ atom_ids ,
521+ {bond_id } if bond_id is not None else set (),
522+ )
523+ atom_idx = parts .parent_atom_ids_by_locant .get (locant )
524+ return (locant ,), {atom_idx } if atom_idx is not None else set (), set ()
525+
526+
527+ def _bond_stereo_tokens (
528+ locant : str ,
529+ descriptor : str ,
530+ atom_ids : set [int ],
531+ bond_ids : set [int ],
532+ ) -> tuple [NameTokenBinding , ...]:
533+ """Return native tokens for an E/Z bond stereochemical descriptor."""
534+
535+ return (
536+ NameTokenBinding (
537+ text = locant ,
538+ token_kind = "locant" ,
539+ ownership = "exact" ,
540+ confidence = "exact" ,
541+ source = "renderer_stereo" ,
542+ grammar_role = "bond_stereo" ,
543+ binding_key = "assembly:bond_stereo" ,
544+ atom_ids = atom_ids ,
545+ bond_ids = bond_ids ,
546+ locants = (locant ,),
547+ ),
548+ NameTokenBinding (
549+ text = descriptor ,
550+ token_kind = "stereo" ,
551+ ownership = "exact" ,
552+ confidence = "exact" ,
553+ source = "renderer_stereo" ,
554+ grammar_role = "bond_stereo" ,
555+ binding_key = "assembly:bond_stereo" ,
556+ atom_ids = atom_ids ,
557+ bond_ids = bond_ids ,
558+ locants = (locant ,),
559+ ),
560+ )
561+
562+
563+ def _relative_stereo_tokens (binding : NameAtomBinding ) -> tuple [NameTokenBinding , ...]:
564+ """Return native token metadata for relative cis/trans descriptors."""
565+
566+ return (
567+ NameTokenBinding (
568+ text = binding .term ,
569+ token_kind = "stereo" ,
570+ ownership = "exact" ,
571+ confidence = "exact" ,
572+ source = "renderer_stereo" ,
573+ grammar_role = binding .role ,
574+ binding_key = f"{ binding .stage } :{ binding .role } " ,
575+ atom_ids = set (binding .atom_ids ),
576+ bond_ids = set (binding .bond_ids ),
577+ charge_atom_ids = set (binding .charge_atom_ids ),
578+ locants = tuple (binding .locants ),
579+ ),
580+ )
581+
582+
433583def _locanted_emitted_tokens (
434584 emitted_tokens : tuple [NameTokenBinding , ...],
435585 locants : tuple [str , ...],
0 commit comments