Skip to content

Commit 809d5e6

Browse files
authored
Merge pull request #416 from CCPBioSim/399-terminal-residues
Residue rotation and UA translation axes for terminal residues that are independent of atom indexing are introduced.
2 parents e494156 + 2b617f1 commit 809d5e6

3 files changed

Lines changed: 654 additions & 196 deletions

File tree

‎CodeEntropy/levels/axes.py‎

Lines changed: 178 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,24 @@ def get_residue_axes(
7373
(previous/next in sequence) using MDAnalysis bonded selections.
7474
- If there are *no* bonds to other residues:
7575
* Use a custom principal axes, from a moment-of-inertia (MOI) tensor
76-
that uses positions of heavy atoms only, but including masses of
76+
that uses positions of heavy atoms only, but includes masses of
7777
heavy atom + bonded hydrogens.
7878
* Set translational axes equal to rotational axes (as per the original
7979
code convention).
80-
- If bonded to other residues:
80+
81+
- If bonded to only one other residue:
82+
* Translational axes are principal axes of data_container.
83+
* Find edge heavy atom (i.e. heavy atoms bonded to neighbour residue).
84+
Compute rotation centre and axes as in get_terminal_axes.
85+
Compute custom MOI, using heavy atom positions and
86+
heavy atom + hydrogen masses.
87+
88+
- If bonded to at least two other residues:
8189
* Translational axes are principal axes of data_container.
82-
* Find edge heavy atoms (i.e. heavy atoms bonded to neighbour residues)
83-
and find the shortest chain between them: the backbone. Edge
84-
atoms + backbone COM are used to determine residue rotational axes.
85-
(see get_residue_custom_axes).Compute a custom MOI, using heavy atom
86-
positions and heavy atom + hydrogen masses.
90+
* Find edge heavy atoms (i.e. heavy atoms bonded to neighbour residues).
91+
Compute rotation centre and axes as in get_non_terminal_axes.
92+
Compute a custom MOI, using heavy atom positions and
93+
heavy atom + hydrogen masses.
8794
8895
Args:
8996
data_container (MDAnalysis.Universe or AtomGroup):
@@ -143,33 +150,19 @@ def get_residue_axes(
143150
else:
144151
make_whole(data_container.atoms)
145152
trans_axes = data_container.atoms.principal_axes()
146-
147153
if len(edge_atom_set) == 1:
148-
if index == 0:
149-
# first residue: use first heavy atom
150-
edges = [residue.atoms[0], edge_atom_set[0]]
151-
backbone = self.get_chain(
152-
residue, residue.atoms[0], edge_atom_set[0]
153-
)
154-
else:
155-
# last residue: last heavy atom
156-
last_index = len(uas) - 1
157-
last = None
158-
if last_index > 0 and last is None:
159-
heavy_atom = uas[last_index]
160-
last = heavy_atom
161-
edges = [edge_atom_set[0], last]
162-
163-
backbone = self.get_chain(residue, edge_atom_set[0], last)
154+
edge_atom = edge_atom_set[0]
155+
rot_center, rot_axes = self.get_terminal_axes(
156+
residue=residue,
157+
edge=edge_atom,
158+
dimensions=data_container.dimensions[:3],
159+
)
164160
else:
165-
edges = [edge_atom_set[0], edge_atom_set[1]]
166-
backbone = self.get_chain(residue, edge_atom_set[0], edge_atom_set[1])
167-
backbone_center = np.zeros(3)
168-
for heavy_atom in backbone:
169-
backbone_center += heavy_atom.position
170-
backbone_center = backbone_center / len(backbone)
171-
rot_center, rot_axes = self.get_residue_custom_axes(edges, backbone_center)
172-
161+
rot_center, rot_axes = self.get_non_terminal_axes(
162+
residue=residue,
163+
edges=edge_atom_set,
164+
dimensions=data_container.dimensions[:3],
165+
)
173166
moment_of_inertia = self.get_custom_residue_moment_of_inertia(
174167
center_of_mass=rot_center,
175168
positions=uas.positions,
@@ -247,18 +240,17 @@ def get_UA_axes(self, data_container, index: int, res_position):
247240
Use the same approach as residue level rotational.
248241
Identify residue of interest and neighbours, then select
249242
edge heavy atoms (i.e. heavy atoms bonded to neighbour residues).
250-
If there are no bonds to neighbouring residues, use residue
251-
.principal axes Otherwise, find the shortest chain between edge
252-
residues: the backbone. Edge atoms + backbone COM are used to
253-
determine UA translational axes (see get_residue_custom_axes)
243+
- If there are *no* bonds to other residues, use a custom principal axes
244+
from a moment-of-inertia (MOI) tensor that uses positions of heavy atoms
245+
only, but includes masses of heavy atom + bonded hydrogens.
246+
- If bonded to only one other residue, see get_terminal_axes.
247+
- If bonded to at least two other residues, see get_non_terminal_axes.
254248
255249
- Rotational axes:
256250
Identify heavy atoms in the residue/molecule of interest and choose
257251
the `index`-th heavy atom (where index corresponds to the bead index).
258252
Use bonded topology around that heavy atom to determine UA rotational
259-
axes (see :meth:`get_bonded_axes`).
260-
Compute a custom MOI tensor using heavy-atom coordinates but UA masses
261-
(heavy + bonded H masses), then compute the principal axes from it.
253+
axes (see :meth:`get_bonded_axes`). Compute a custom MOI tensor.
262254
263255
Args:
264256
data_container (MDAnalysis.Universe or AtomGroup):
@@ -290,71 +282,51 @@ def get_UA_axes(self, data_container, index: int, res_position):
290282
residue = data_container
291283
trans_center = data_container.atoms.center_of_mass(unwrap=True)
292284
trans_axes = data_container.atoms.principal_axes()
293-
residue_heavy_atoms = heavy_atoms
294285
else:
295286
# residue of interest has at least one neighbour
296-
if res_position == -1:
297-
residue = data_container.residues[0]
298-
resindex = residue.resindex
299-
resindex_next = resindex + 1
300-
301-
second_edge = data_container.select_atoms(
302-
f"resindex {resindex} and bonded resindex {resindex_next}"
287+
if res_position == -1 or res_position == 1:
288+
# look at a terminal residue
289+
if res_position == -1:
290+
# first residue
291+
residue = data_container.residues[0]
292+
resindex = residue.resindex
293+
resindex_next = resindex + 1
294+
edge_atom_set = data_container.select_atoms(
295+
f"resindex {resindex} and bonded resindex {resindex_next}"
296+
)
297+
else:
298+
# last residue
299+
residue = data_container.residues[1]
300+
resindex = residue.resindex
301+
resindex_prev = resindex - 1
302+
edge_atom_set = data_container.select_atoms(
303+
f"resindex {resindex} and bonded resindex {resindex_prev}"
304+
)
305+
edge_atom = edge_atom_set[0]
306+
trans_center, trans_axes = self.get_terminal_axes(
307+
residue=residue,
308+
edge=edge_atom,
309+
dimensions=data_container.dimensions[:3],
303310
)
304-
305-
edges = [residue.atoms[0], second_edge[0]]
306-
backbone = self.get_chain(
307-
residue, residue.atoms[0], second_edge.atoms[0]
308-
)
309-
310-
elif res_position == 0:
311+
else:
311312
# between 2 residues
312313
residue = data_container.residues[1]
313314
resindex = residue.resindex
314315
resindex_next = resindex + 1
315316
resindex_prev = resindex - 1
316-
317-
edge_set = data_container.select_atoms(
317+
edge_atom_set = data_container.select_atoms(
318318
f"resindex {resindex} and "
319319
f"(bonded resindex {resindex_prev} or "
320320
f"resindex {resindex_next})"
321321
)
322-
323-
edges = [edge_set[0], edge_set[1]]
324-
backbone = self.get_chain(residue, edge_set[0], edge_set[1])
325-
326-
else:
327-
# last resid
328-
# always resindex 1 in data_container
329-
residue = data_container.residues[1]
330-
resindex = residue.resindex
331-
resindex_prev = resindex - 1
332-
first_edge = data_container.select_atoms(
333-
f"resindex {resindex} and bonded resindex {resindex_prev}"
322+
trans_center, trans_axes = self.get_non_terminal_axes(
323+
residue=residue,
324+
edges=edge_atom_set,
325+
dimensions=data_container.dimensions[:3],
334326
)
335-
336-
last_index = len(heavy_atoms) - 1
337-
last = None
338-
# look for last heavy atom
339-
# with only one bond to another
340-
if last_index > 0 and last is None:
341-
heavy_atom = heavy_atoms[last_index]
342-
last = heavy_atom
343-
344-
edges = [first_edge.atoms[0], last]
345-
backbone = self.get_chain(residue, first_edge.atoms[0], last)
346-
347-
backbone_center = np.zeros(3)
348-
for heavy_atom in backbone:
349-
backbone_center += heavy_atom.position
350-
backbone_center = backbone_center / len(backbone)
351-
352-
trans_center, trans_axes = self.get_residue_custom_axes(
353-
edges, backbone_center
354-
)
355-
residue_heavy_atoms = residue.atoms.select_atoms("mass 2 to 999")
356-
357327
# look for heavy atoms in residue of interest
328+
residue_heavy_atoms = residue.atoms.select_atoms("mass 2 to 999")
329+
358330
heavy_atom_indices = []
359331
for atom in residue_heavy_atoms:
360332
heavy_atom_indices.append(atom.index)
@@ -579,10 +551,14 @@ def get_residue_custom_axes(self, edges, center):
579551
rot_center: (3,) rotation centre,
580552
lies on the E1-E2 vector
581553
rot_axes: (3,3) rotation axes of residue
554+
555+
Raises:
556+
ValueError: If axes cannot be normalized due to degeneracy.
557+
582558
"""
583-
first_edge_centre_of_geometry_vector = center - edges[0].position
559+
first_edge_centre_of_geometry_vector = center - edges[0]
584560
# look for projection of E1-O onto E1-E2 (E1-C)
585-
first_edge_second_edge_vector = edges[1].position - edges[0].position
561+
first_edge_second_edge_vector = edges[1] - edges[0]
586562
first_edge_origin_vector = (
587563
np.dot(first_edge_second_edge_vector, first_edge_centre_of_geometry_vector)
588564
/ (np.linalg.norm(first_edge_second_edge_vector) ** 2)
@@ -594,15 +570,119 @@ def get_residue_custom_axes(self, edges, center):
594570
)
595571
y_axis = origin_centre_of_geometry_vector
596572
z_axis = np.cross(x_axis, y_axis)
597-
x_axis /= np.linalg.norm(x_axis)
598-
y_axis /= np.linalg.norm(y_axis)
599-
z_axis /= np.linalg.norm(z_axis)
600-
rot_axes = np.array([x_axis, y_axis, z_axis])
601-
rot_center = first_edge_origin_vector + edges[0].position
573+
unscaled_rot_axes = np.array((x_axis, y_axis, z_axis), dtype=float)
574+
mod = np.sqrt(np.sum(unscaled_rot_axes**2, axis=1))
575+
if np.any(np.isclose(mod, 0.0)):
576+
raise ValueError("Degenerate custom axes: cannot normalize (zero norm).")
577+
rot_axes = unscaled_rot_axes / mod[:, np.newaxis]
578+
rot_center = first_edge_origin_vector + edges[0]
579+
return rot_center, rot_axes
580+
581+
def get_terminal_axes(self, residue, edge, dimensions):
582+
"""
583+
Compute rotation axes at the residue level/translation axes at the UA level
584+
for the terminal residues in a polymer, given the edge atom
585+
(i.e. atom bonded to neighbour residue) and residue of interest.
586+
Find all heavy atoms bonded to edge heavy atom and compute
587+
their average position. Find all other heavy atoms in residue
588+
and compute their average position. The three points are now used to
589+
obtain determine residue rotational axes. (see get_residue_custom_axes)
590+
If there are only two heavy atoms in the residue/all heavy atoms are bonded
591+
to edge atom, x-axis is set along the vector between the
592+
edge atom and average position of bonded atoms, y-axis is arbitrary
593+
and z-axis is paralel to the two. This is the same as case 2 in get_bonded_axes.
594+
If there no heavy atoms bonded to the edge atom (i.e. the edge atom is the only
595+
heavy atom in the residue), centre is set on edge atom and axes are principal
596+
axes.
597+
598+
Args:
599+
residue: MDAnalysis AtomGroup
600+
edge: MDAnalysis atom
601+
dimensions: (3,) dimensions of the simulation box
602+
603+
Returns:
604+
rot_center: (3,) rotation centre,
605+
rot_axes: (3,3) rotation axes of residue
606+
"""
607+
heavy_atoms = residue.atoms.select_atoms("mass 2 to 999")
608+
bonded_atoms = residue.atoms.select_atoms(
609+
f"(mass 2 to 999) and bonded index {edge.index}"
610+
)
611+
if len(bonded_atoms) == 0:
612+
# there is only one heavy atom in the residue
613+
rot_center = edge.position
614+
rot_axes = residue.atoms.principal_axes()
615+
else:
616+
average_bonded = np.zeros(3)
617+
for bonded_atom in bonded_atoms:
618+
average_bonded += bonded_atom.position
619+
average_bonded /= len(bonded_atoms)
620+
# find the average position of all other heavy atoms in residue
621+
other_atoms = []
622+
for atom in heavy_atoms:
623+
if atom != edge and atom not in bonded_atoms:
624+
other_atoms.append(atom)
625+
if len(other_atoms) > 0:
626+
average_other_atoms = np.zeros(3)
627+
for atom in other_atoms:
628+
average_other_atoms += atom.position
629+
average_other_atoms /= len(other_atoms)
630+
rot_center, rot_axes = self.get_residue_custom_axes(
631+
[edge.position, average_other_atoms], average_bonded
632+
)
633+
else:
634+
rot_center = edge.position
635+
rot_axes = self.get_custom_axes(
636+
a=edge.position,
637+
b_list=[average_bonded],
638+
c=np.zeros(3),
639+
dimensions=dimensions,
640+
)
641+
return rot_center, rot_axes
642+
643+
def get_non_terminal_axes(self, residue, edges, dimensions):
644+
"""
645+
Compute rotation axes at the residue level/ translation axes at
646+
the UA level for the non-terminal residues in a linear polymer, given the
647+
edge atoms (i.e. heavy atoms bonded to neighbour residues) and
648+
residue of interest. Find the shortest chain between edge atoms: the backbone.
649+
Edges + backbone average position determine the residue rotational axes.
650+
(see get_residue_custom_axes). If the two edge heavy atoms
651+
are bonded to each other (i.e. there is no backbone), x-axis is set
652+
along the vector between the edge atom and average position of bonded
653+
atoms, y-axis is arbitrary and z-axis is paralel to the two. This is the
654+
same as case 2 in get_bonded_axes.
655+
Args:
656+
residue: MDAnalysis AtomGroup
657+
edges: MDAnalysis AtomGroup
658+
dimensions: (3,) dimensions of the simulation box
659+
660+
Returns:
661+
rot_center: (3,) rotation centre,
662+
rot_axes: (3,3) rotation axes of residue
663+
"""
664+
backbone = self.get_chain(residue, edges[0], edges[1])
665+
backbone_center = np.zeros(3)
666+
if len(backbone) > 0:
667+
for heavy_atom in backbone:
668+
backbone_center += heavy_atom.position
669+
backbone_center /= len(backbone)
670+
rot_center, rot_axes = self.get_residue_custom_axes(
671+
edges.positions, backbone_center
672+
)
673+
else:
674+
rot_center = (edges[0].position + edges[1].position) / 2
675+
rot_axes = self.get_custom_axes(
676+
a=rot_center,
677+
b_list=[edges[0].position],
678+
c=np.zeros(3),
679+
dimensions=dimensions,
680+
)
602681
return rot_center, rot_axes
603682

604683
def get_bonded_axes(self, system, atom, dimensions: np.ndarray):
605-
r"""Compute UA rotational axes from bonded topology around a heavy atom.
684+
"""
685+
Compute UA rotational axes from bonded topology around a heavy atom.
606686
607687
For a given heavy atom, use its bonded atoms to get the axes for rotating
608688
forces around. Few cases for choosing united atom axes, which are dependent

‎docs/science.rst‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ The axes for this transformation are calculated for each bead in each time step.
7070

7171
For the polymer level, the translational and rotational axes are defined as the principal axes of the molecule.
7272

73-
For the residue level, there are two situations.
73+
For the residue level, there are three situations.
7474
When the residue is not bonded to any other residues, the translational and rotational axes are the principal axes of the molecule.
75-
When the residue is part of a larger polymer, the translational axes are the principal axes of the polymer, and the rotational axes are defined from the two heavy atoms bonded to neighbour residues(E1,E2) and the average position of all other backbone atoms in the residue (C). The backbone of a residue is defined as the shortest path between the two edge atoms of the residue, i.e. the two heavy atoms bonded to neighbour residues.The centre of rotation is located at the point where the perpendicular from C meets the E1-E2 vector.
75+
When the residue is part of a larger polymer and is not a terminus of that polymer, the translational axes are the principal axes of the polymer, and the rotational axes are defined from the two heavy atoms bonded to neighbour residues (E1,E2) and the average position of all other backbone atoms in the residue (C). The backbone of a residue is defined as the shortest path between the two edge atoms of the residue, i.e.the two heavy atoms bonded to neighbour residues.The centre of rotation (O) is located at the point where the perpendicular from C meets the E1-E2 vector.
76+
When the residue is part of a larger polymer and is a terminus of that polymer, the translational axes are the principal axes of the polymer, and the rotational axes are defined from the heavy atom bonded to a
77+
neighbour residue (E1), the average position of all heavy atoms bonded to E1 (C) and the average position of all other heavy atoms in the residue (E2). The centre of rotation (O) is defined the same as above forthe non-terminal residue case.
7678

7779
For the united atom level, the translational axes are defined as the residue rotational axes and the rotational axes are defined from the average position of the bonds to neighbouring heavy atoms.
7880
If there are no bonds to other heavy atoms, the principal axes of the molecule are used.

0 commit comments

Comments
 (0)