Skip to content

Commit fe9c7e5

Browse files
committed
f zmat
1 parent 9dd39f8 commit fe9c7e5

1 file changed

Lines changed: 59 additions & 42 deletions

File tree

arc/species/zmat.py

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nskipped atoms are:\n{skipped_atoms}.")
157-
156+
f"a constraint lock. The partial zmat is:\n{zmat}\n\nskipped 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]],
14421428
def 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+
16411658
def get_atom_order_from_xyz(xyz: Dict[str, tuple],
16421659
fragment: Optional[List[int]] = None,
16431660
) -> List[int]:

0 commit comments

Comments
 (0)