@@ -153,15 +153,14 @@ def xyz_to_zmat(xyz: Dict[str, tuple],
153153 if num_of_skipped_atoms == len (skipped_atoms ):
154154 # No atoms were popped from the skipped atoms list when iterating through all skipped atoms.
155155 raise ZMatError (f"Could not generate the zmat, skipped atoms could not be assigned, there's probably "
156- f"a constraint lock. The partial zmat is:\n { zmat } \n \n skipped atoms are:\n { skipped_atoms } ." )
157-
156+ f"a constraint lock. The partial zmat is:\n { zmat } \n \n skipped atoms are:\n { skipped_atoms } .\n "
157+ f"constraints were: \n { constraints } " )
158158 if consolidate and not constraints :
159159 try :
160160 zmat = consolidate_zmat (zmat , mol , consolidation_tols )
161161 except (KeyError , ZMatError ) as e :
162162 logger .error (f'Could not consolidate zmat, got:\n { e .__class__ } : { str (e )} ' )
163163 logger .error (f'Generating zmat without consolidation.' )
164-
165164 zmat ['symbols' ] = tuple (zmat ['symbols' ])
166165 zmat ['coords' ] = tuple (zmat ['coords' ])
167166 return zmat
@@ -389,14 +388,7 @@ def determine_a_atoms(zmat: Dict[str, Union[dict, tuple]],
389388 # Atom B might be in a linear chain, determine the A -- B -- C angle.
390389 b_neighbors = connectivity [atom_b ]
391390 atom_a = b_neighbors [0 ] if b_neighbors [0 ] != atom_c else b_neighbors [1 ]
392- angle = calculate_param (coords = coords , atoms = [atom_a , atom_b , atom_c ])
393- if is_angle_linear (angle , tolerance = TOL_180 ):
394- # A -- B -- C is linear, change indices and test angle E -- A -- B instead.
395- atom_c = atom_b
396- atom_b = atom_a
397- elif key_by_val (zmat ['map' ], atom_b ) not in a_atoms :
398- # A -- B -- C is not linear, use atom B.
399- zmat_index = key_by_val (zmat ['map' ], atom_b )
391+ zmat_index = key_by_val (zmat ['map' ], atom_b )
400392 elif num_of_neighbors > 2 :
401393 # Atom B does not necessarily lead to a linear A -- B -- C chain, no need to intervene.
402394 zmat_index = key_by_val (zmat ['map' ], atom_b )
@@ -431,15 +423,9 @@ def determine_a_atoms(zmat: Dict[str, Union[dict, tuple]],
431423 if j != i and atom_a not in [atom_b , atom_c ] \
432424 and (j in list (zmat ['map' ].keys ()) and not is_dummy (zmat , j )
433425 or j not in list (zmat ['map' ].keys ())):
434- angle = calculate_param (coords = coords , atoms = [atom_a , atom_b , atom_c ])
435- if is_angle_linear (angle , tolerance = TOL_180 ):
436- # A -- B -- C is linear, change indices and test angle E -- A -- B.
437- atom_b = atom_a
438- elif zmat_index not in a_atoms :
439- # A -- B -- C is not linear, use atom B.
440- zmat_index = key_by_val (zmat ['map' ], atom_b )
441- a_atoms .append (zmat_index )
442- break
426+ zmat_index = key_by_val (zmat ['map' ], atom_b )
427+ a_atoms .append (zmat_index )
428+ break
443429 j -= 1 # Don't loop forever.
444430 if len (a_atoms ) == 3 :
445431 break
@@ -746,6 +732,7 @@ def _add_nth_atom_to_zmat(zmat: Dict[str, Union[dict, tuple]],
746732 - The xyz coordinates updated with dummy atoms.
747733 - A 0- or 1-length list with the skipped atom index.
748734 """
735+ num_initial_atoms = len (xyz ['symbols' ])
749736 coords = xyz ['coords' ]
750737 skipped_atoms = list ()
751738 specific_last_d_atom = None
@@ -770,7 +757,7 @@ def _add_nth_atom_to_zmat(zmat: Dict[str, Union[dict, tuple]],
770757
771758 # If an '_atom' constraint was specified, only consider this atom if n is the last atom to consider.
772759 if (r_constraint_type == 'R_atom' or a_constraint_type == 'A_atom' or d_constraint_type == 'D_atom' ) \
773- and n != len ([ symbol for symbol in xyz [ 'symbols' ] if 'X' not in symbol ]) - 1 :
760+ and n != num_initial_atoms - 1 :
774761 logger .debug (f'Skipping atom index { atom_index } when creating a zmat due to a specified _atom constraint.' )
775762 skipped_atoms .append (atom_index )
776763 return zmat , xyz , skipped_atoms
@@ -805,7 +792,6 @@ def _add_nth_atom_to_zmat(zmat: Dict[str, Union[dict, tuple]],
805792 trivial_assignment = any ('_atom' in constraint_key for constraint_key in constraints .keys ()),
806793 fragments = fragments ,
807794 )
808-
809795 # Calculate the angle, add a dummy atom if needed.
810796 added_dummy = False
811797 if a_atoms is not None and all ([not re .match (r'X\d' , str (zmat ['map' ][atom ])) for atom in a_atoms [1 :]]):
@@ -1408,7 +1394,7 @@ def get_atom_order(xyz: Optional[Dict[str, tuple]] = None,
14081394 atom_order = list ()
14091395 if mol is not None :
14101396 for fragment in fragments :
1411- sequence = get_atom_order_from_mol (mol = mol , fragment = fragment , constraints_dict = constraints_dict )
1397+ sequence = get_atom_order_from_mol (mol = mol , fragment = fragment , constraints_dict = constraints_dict , xyz = xyz )
14121398 for i in sequence :
14131399 if i not in atom_order :
14141400 atom_order .append (i )
@@ -1442,6 +1428,7 @@ def _accumulated_atom_order(fragments: List[List[int]],
14421428def get_atom_order_from_mol (mol : Molecule ,
14431429 fragment : List [int ] = None ,
14441430 constraints_dict : Optional [Dict [str , List [tuple ]]] = None ,
1431+ xyz : Optional [dict ] = None ,
14451432 ) -> List [int ]:
14461433 """
14471434 Determine Z-matrix atom order based on molecular connectivity and an optional single constraint.
@@ -1462,6 +1449,7 @@ def get_atom_order_from_mol(mol: Molecule,
14621449 'D_atom': [(move_idx, ref1, ref2, ref3)]
14631450 'D_group': [(move_idx, ref1, ref2, ref3)]
14641451 'D_groups': [(pivot2, pivot3, ref?, ref?)]
1452+ xyz (dict, optional): Optional 3D coordinates dict, used for checking linear motifs to avoid starting with them.
14651453
14661454 Returns:
14671455 A list of atom indices in Z-matrix order, where any unconstrained
@@ -1516,32 +1504,40 @@ def get_atom_order_from_mol(mol: Molecule,
15161504
15171505 # 3) Base BFS ordering on heavy-atom graph
15181506 # find start: a heavy with <=1 heavy neighbor not in constrained_set if possible
1519- heavy_neighbors = lambda a : sum (nb .is_non_hydrogen () for nb in a .edges )
1520- start = None
1521- for atom in mol .atoms :
1522- i = mol .atoms .index (atom )
1523- if i in fragment and not atom .is_hydrogen () and i not in constrained_set and heavy_neighbors (atom ) <= 1 :
1524- start = i
1525- break
1526- if start is None :
1507+ # Avoid atoms that are A in a linear A–B–C motif
1508+
1509+ def find_start (avoid_linear : bool = True ) -> Optional [int ]:
1510+ """Find a suitable start atom in the fragment."""
15271511 for atom in mol .atoms :
15281512 i = mol .atoms .index (atom )
1529- if i in fragment and not atom .is_hydrogen () and i not in constrained_set :
1530- start = i
1531- break
1532- if start is None :
1533- # last resort: any heavy
1513+ if (
1514+ i in fragment and
1515+ not atom .is_hydrogen () and
1516+ i not in constrained_set and
1517+ (not avoid_linear or not is_atom_in_linear_angle (i = i , xyz = xyz , mol = mol )) and
1518+ sum (nb .is_non_hydrogen () for nb in atom .edges ) <= 1
1519+ ):
1520+ return i
1521+ for atom in mol .atoms :
1522+ i = mol .atoms .index (atom )
1523+ if (
1524+ i in fragment and
1525+ not atom .is_hydrogen () and
1526+ i not in constrained_set and
1527+ (not avoid_linear or not is_atom_in_linear_angle (i = i , xyz = xyz , mol = mol ))
1528+ ):
1529+ return i
15341530 for atom in mol .atoms :
15351531 i = mol .atoms .index (atom )
15361532 if i in fragment and not atom .is_hydrogen ():
1537- start = i
1538- break
1539- if start is None :
1540- # no heavy found, pick any atom
1541- start = next ( iter ( fragment ) )
1533+ return i
1534+ return next ( iter ( fragment ))
1535+
1536+ # Attempt to find a non-linear start; fall back if needed
1537+ start = find_start ( avoid_linear = True )
15421538
15431539 visited = set ()
1544- base_heavies = []
1540+ base_heavies = list ()
15451541 queue = [start ]
15461542 while queue :
15471543 i = queue .pop (0 )
@@ -1638,6 +1634,27 @@ def get_atom_order_from_mol(mol: Molecule,
16381634 return atom_order
16391635
16401636
1637+ def is_atom_in_linear_angle (i : int , xyz : Optional [dict ], mol : Molecule , tol : float = 0.9 ) -> bool :
1638+ """
1639+ Check if atom i is involved in a linear A–B–C motif (i.e., angle ~180),
1640+ whether as A, B, or C.
1641+ """
1642+ if not xyz :
1643+ return False
1644+ for b in range (len (mol .atoms )):
1645+ b_neighbors = [mol .atoms .index (atom ) for atom in mol .atoms [b ].edges ]
1646+ for a in b_neighbors :
1647+ for c in b_neighbors :
1648+ if a >= c :
1649+ continue # avoid redundant pairs and self-pairs
1650+ if i not in (a , b , c ):
1651+ continue
1652+ angle = calculate_param (coords = xyz ['coords' ], atoms = [a , b , c ])
1653+ if is_angle_linear (angle , tolerance = tol ):
1654+ return True
1655+ return False
1656+
1657+
16411658def get_atom_order_from_xyz (xyz : Dict [str , tuple ],
16421659 fragment : Optional [List [int ]] = None ,
16431660 ) -> List [int ]:
0 commit comments