|
86 | 86 | from .topology import Topology
|
87 | 87 | from .topologyattrs import (
|
88 | 88 | AtomAttr, ResidueAttr, SegmentAttr,
|
| 89 | + Segindices, Segids, Resindices, Resids, Atomindices, |
89 | 90 | BFACTOR_WARNING, _Connection
|
90 | 91 | )
|
91 | 92 | from .topologyobjects import TopologyObject
|
|
94 | 95 | logger = logging.getLogger("MDAnalysis.core.universe")
|
95 | 96 |
|
96 | 97 |
|
| 98 | +def _update_topology_by_ids(universe, atomwise_resids, atomwise_segids): |
| 99 | + """Update the topology of a Universe with new atomwise resids and segids. |
| 100 | +
|
| 101 | + Parameters |
| 102 | + ---------- |
| 103 | + universe : Universe |
| 104 | + The universe to update. |
| 105 | + atomwise_resids : numpy.ndarray |
| 106 | + The new atomwise residue indices. |
| 107 | + atomwise_segids : numpy.ndarray |
| 108 | + The new atomwise segment indices. |
| 109 | + """ |
| 110 | + from ..topology.base import change_squash |
| 111 | + |
| 112 | + # the original topology |
| 113 | + top = universe._topology |
| 114 | + |
| 115 | + # detect the atom level attributes (excluding residue level and above) |
| 116 | + atom_attrindices = [ |
| 117 | + idx |
| 118 | + for idx, each_attr in enumerate(top.attrs) |
| 119 | + if (Residue not in each_attr.target_classes) and |
| 120 | + (not isinstance(each_attr, Atomindices)) |
| 121 | + ] |
| 122 | + attrs = [top.attrs[each_attr] for each_attr in atom_attrindices] |
| 123 | + |
| 124 | + # create new residues level stuff |
| 125 | + residue_attrindices = [ |
| 126 | + idx |
| 127 | + for idx, each_attr in enumerate(top.attrs) |
| 128 | + if ( |
| 129 | + (Residue in each_attr.target_classes) and |
| 130 | + (Segment not in each_attr.target_classes) and |
| 131 | + (not isinstance(each_attr, Resids)) and |
| 132 | + (not isinstance(each_attr, Resindices)) |
| 133 | + ) |
| 134 | + ] # residue level attributes except resids and resindices |
| 135 | + |
| 136 | + res_criteria = [atomwise_resids, atomwise_segids] + [ |
| 137 | + getattr(universe.atoms, top.attrs[each_attr].attrname) |
| 138 | + for each_attr in residue_attrindices |
| 139 | + if top.attrs[each_attr].attrname != 'resnums' |
| 140 | + ] |
| 141 | + |
| 142 | + res_to_squash = [atomwise_resids, atomwise_segids] + [ |
| 143 | + getattr(universe.atoms, top.attrs[each_attr].attrname) |
| 144 | + for each_attr in residue_attrindices |
| 145 | + ] |
| 146 | + |
| 147 | + residx, res_squashed = change_squash(res_criteria, res_to_squash) |
| 148 | + resids = res_squashed[0] |
| 149 | + res_squashed_segids = res_squashed[1] |
| 150 | + n_residues = len(resids) |
| 151 | + # all residue-level attributes except resids |
| 152 | + res_squashed_res_attrs = res_squashed[2:] |
| 153 | + |
| 154 | + res_attrs = [Resids(resids)] + [ |
| 155 | + # use the correspdoning type of the attribute with new sqaushed values |
| 156 | + top.attrs[each_attr].__class__(res_squashed_res_attrs[idx]) |
| 157 | + for idx, each_attr in enumerate(residue_attrindices) |
| 158 | + ] |
| 159 | + attrs.extend(res_attrs) |
| 160 | + |
| 161 | + # create new segment level stuff |
| 162 | + segidx, (segids,) = change_squash((res_squashed_segids,), (res_squashed_segids,)) |
| 163 | + n_segments = len(segids) |
| 164 | + attrs.append(Segids(segids)) |
| 165 | + |
| 166 | + # other attributes |
| 167 | + other_attrs = [ |
| 168 | + each_attr |
| 169 | + for each_attr in top.attrs |
| 170 | + if ( |
| 171 | + (Segment in each_attr.target_classes) and |
| 172 | + (not isinstance(each_attr, Segids)) and |
| 173 | + (not isinstance(each_attr, Atomindices)) and |
| 174 | + (not isinstance(each_attr, Resindices)) and |
| 175 | + (not isinstance(each_attr, Segindices)) |
| 176 | + ) |
| 177 | + ] |
| 178 | + |
| 179 | + # create new topology |
| 180 | + top = Topology( |
| 181 | + universe.atoms.n_atoms, |
| 182 | + n_residues, |
| 183 | + n_segments, |
| 184 | + attrs=attrs, |
| 185 | + atom_resindex=residx, |
| 186 | + residue_segindex=segidx, |
| 187 | + ) |
| 188 | + |
| 189 | + # add back other attributes |
| 190 | + if len(other_attrs) > 0: |
| 191 | + for each_otherattr in other_attrs: |
| 192 | + top.add_TopologyAttr(each_otherattr) |
| 193 | + |
| 194 | + # update the topology in the universe |
| 195 | + universe._topology = top |
| 196 | + |
97 | 197 |
|
98 | 198 | def _check_file_like(topology):
|
99 | 199 | if isstream(topology):
|
@@ -312,6 +412,12 @@ class Universe(object):
|
312 | 412 | functionality to treat independent trajectory files as a single virtual
|
313 | 413 | trajectory.
|
314 | 414 | **kwargs: extra arguments are passed to the topology parser.
|
| 415 | + For instance, when reading a PDB file |
| 416 | + (:class:`PDBReader<MDAnalysis.coordinates.PDB>`, |
| 417 | + :class:`PDBParser<MDAnalysis.topology.PDBParser>`), set |
| 418 | + ``force_chainids_to_segids=True`` to make the universe use the |
| 419 | + chainIDs (column 22) instead of the segmentIDs (column 73-76) as the |
| 420 | + `segids` in the universe and select the corresponding SegmentGroup. |
315 | 421 |
|
316 | 422 | Attributes
|
317 | 423 | ----------
|
@@ -380,6 +486,10 @@ class Universe(object):
|
380 | 486 | guessing masses and atom types after topology
|
381 | 487 | is read from a registered parser.
|
382 | 488 |
|
| 489 | + .. versionchanged:: 2.10.0 |
| 490 | + Added :meth: `~MDAnalysis.core.universe.Universe.set_groups` |
| 491 | + API to set residues/segments based on the atomwise resids/segids. |
| 492 | +
|
383 | 493 | """
|
384 | 494 | def __init__(self, topology=None, *coordinates, all_coordinates=False,
|
385 | 495 | format=None, topology_format=None, transformations=None,
|
@@ -1700,6 +1810,104 @@ def guess_TopologyAttrs(
|
1700 | 1810 | warnings.warn('Can not guess attributes '
|
1701 | 1811 | 'for universe with 0 atoms')
|
1702 | 1812 |
|
| 1813 | + def set_groups(self, atomwise_resids=None, atomwise_segids=None): |
| 1814 | + """Set the groups (`ResidueGroup`, `SegmentGroup`) of the Universe |
| 1815 | + by atomwise resids/segids. |
| 1816 | +
|
| 1817 | + The `topology` will also be updated based on the provided `atomwise_resids` |
| 1818 | + and `atomwise_segids`. The original `resids` and `segids` will be stored |
| 1819 | + in attributes `atomwise_resids_orig` and/or `atomwise_segids_orig` if |
| 1820 | + they are modified. |
| 1821 | + See notes for the logic of the function. |
| 1822 | +
|
| 1823 | + Parameters |
| 1824 | + ---------- |
| 1825 | + atomwise_resids: |
| 1826 | + A list of residue IDs to be set for the Universe. The length |
| 1827 | + of the list should be equal to the number of atoms in the Universe. |
| 1828 | + If `None`, the original resids will be used. |
| 1829 | +
|
| 1830 | + atomwise_segids: |
| 1831 | + A list of segment IDs to be set for the Universe. The length |
| 1832 | + of the list should be equal to the number of atoms in the Universe. |
| 1833 | + If `None`, the original segids will be used. |
| 1834 | +
|
| 1835 | + Raises |
| 1836 | + ------ |
| 1837 | + AssertionError |
| 1838 | + If the length of the provided atomwise_resids or atomwise_segids |
| 1839 | + does not match the number of atoms in the Universe. |
| 1840 | +
|
| 1841 | + Notes |
| 1842 | + ----- |
| 1843 | + First, the function will check if resids or segids is provided. |
| 1844 | + If both resids and segids are not provided (`None`), it will do nothing. |
| 1845 | + If only one of them is provided, it will use the original values for the |
| 1846 | + other one. If both are provided, it will use the provided values for |
| 1847 | + both resids and segids. |
| 1848 | + The function will then update the topology by a new generated topology |
| 1849 | + with new values of the resids and segids. |
| 1850 | + Finally, the corresponding new `ResidueGroup` and `SegmentGroup` will be |
| 1851 | + created by the updated topology. |
| 1852 | +
|
| 1853 | + Examples |
| 1854 | + -------- |
| 1855 | + To set custom segment IDs for the segments of the Universe:: |
| 1856 | +
|
| 1857 | + atomwise_segids = ['A', 'A', 'B', 'B'] |
| 1858 | + u.set_groups(atomwise_segids=atomwise_segids) |
| 1859 | +
|
| 1860 | + # Now the Universe has two segments with segIDs 'A' and 'B' |
| 1861 | + u.segments |
| 1862 | + >>> <SegmentGroup with 2 segments> |
| 1863 | +
|
| 1864 | + .. versionadded:: 2.10.0 |
| 1865 | + """ |
| 1866 | + if (atomwise_resids is None) and (atomwise_segids is None): |
| 1867 | + warnings.warn("Not setting groups. Please provide atomwise_resids or " |
| 1868 | + "atomwise_segids.") |
| 1869 | + return |
| 1870 | + |
| 1871 | + # resids |
| 1872 | + if atomwise_resids is None: |
| 1873 | + atomwise_resids = self.atoms.resids |
| 1874 | + |
| 1875 | + else: |
| 1876 | + # check the length of atomwise_resids |
| 1877 | + if len(atomwise_resids) != self.atoms.n_atoms: |
| 1878 | + raise ValueError( |
| 1879 | + "The length of atomwise_resids should be the same as " |
| 1880 | + "the number of atoms in the universe.") |
| 1881 | + |
| 1882 | + self.atomwise_resids_orig = self.atoms.resids |
| 1883 | + logger.info("The new resids replaces the current one. " |
| 1884 | + "The original resids is stored in " |
| 1885 | + "atomwise_resids_orig.") |
| 1886 | + |
| 1887 | + # segids |
| 1888 | + if atomwise_segids is None: |
| 1889 | + atomwise_segids = self.atoms.segids |
| 1890 | + |
| 1891 | + else: |
| 1892 | + # check the length of atomwise_segids |
| 1893 | + if len(atomwise_segids) != self.atoms.n_atoms: |
| 1894 | + raise ValueError( |
| 1895 | + "The length of atomwise_segids should be the same as " |
| 1896 | + "the number of atoms in the universe.") |
| 1897 | + |
| 1898 | + self.atomwise_segids_orig = self.atoms.segids |
| 1899 | + logger.info("The new resids replaces the current one. " |
| 1900 | + "The original segids is stored in " |
| 1901 | + "atomwise_segids_orig.") |
| 1902 | + |
| 1903 | + atomwise_resids = np.array(atomwise_resids, dtype=int) |
| 1904 | + atomwise_segids = np.array(atomwise_segids, dtype=object) |
| 1905 | + |
| 1906 | + _update_topology_by_ids(self, |
| 1907 | + atomwise_resids=atomwise_resids, |
| 1908 | + atomwise_segids=atomwise_segids) |
| 1909 | + _generate_from_topology(self) |
| 1910 | + |
1703 | 1911 |
|
1704 | 1912 | def Merge(*args):
|
1705 | 1913 | """Create a new new :class:`Universe` from one or more
|
|
0 commit comments