"""
residues
========
The `residues` module is a part of the `protflow` package and is designed to handle residue selection and related operations in protein structures. This module provides functionality to parse, manipulate, and convert residue selections in various formats, making it an essential tool for bioinformatics and computational biology workflows.
The module includes the `ResidueSelection` class for representing and manipulating selections of residues, as well as various functions for parsing and converting residue selections.
Classes
-------
- `ResidueSelection`
Represents a selection of residues with functionality for parsing, converting, and manipulating selections.
Functions
---------
- `fast_parse_selection`
Fast parser for selections already in `ResidueSelection` format.
- `parse_selection`
Parses a selection into `ResidueSelection` formatted selection.
- `parse_residue`
Parses a single residue identifier into a tuple (chain, residue_index).
- `residue_selection`
Creates a `ResidueSelection` from a selection of residues.
- `from_dict`
Creates a `ResidueSelection` object from a dictionary specifying a motif.
- `from_contig`
Creates a `ResidueSelection` object from a contig string.
- `reduce_to_unique`
Reduces an input array to its unique elements while preserving order.
Example Usage
-------------
Creating and manipulating `ResidueSelection` objects:
.. code-block:: python
from residues import ResidueSelection, from_dict, from_contig
# Create a ResidueSelection from a list
selection = ResidueSelection(["A1", "A2", "B3"])
# Convert to string
selection_str = selection.to_string()
print(selection_str)
# Output: A1, A2, B3
# Convert to dictionary
selection_dict = selection.to_dict()
print(selection_dict)
# Output: {'A': [1, 2], 'B': [3]}
# Create a ResidueSelection from a dictionary
selection_from_dict = from_dict({"A": [1, 2], "B": [3]})
print(selection_from_dict.to_string())
# Output: A1, A2, B3
# Create a ResidueSelection from a contig string
selection_from_contig = from_contig("A1-A3, B5")
print(selection_from_contig.to_string())
# Output: A1, A2, A3, B5
This module simplifies the process of handling residue selections in bioinformatics workflows, providing a consistent interface for different types of input and output formats.
"""
# imports
from collections import OrderedDict, defaultdict
[docs]
class ResidueSelection:
"""
ResidueSelection
================
A class to represent selections of residues in protein structures. A selection of residues is represented
as a tuple with the hierarchy ((chain, residue_idx), ...).
Parameters
----------
selection : list, optional
A list of residues in string format, e.g., ["A1", "A2", "B3"]. Default is None.
delim : str, optional
The delimiter used to parse the selection string. Default is ",".
fast : bool, optional
If True, parses the selection without any type checking. Use when `selection` is already in
ResidueSelection format. Default is False.
Attributes
----------
residues : tuple
A tuple representing the parsed residues selection.
Examples
--------
>>> from residues import ResidueSelection
>>> selection = ResidueSelection(["A1", "A2", "B3"])
>>> print(selection.to_string())
A1, A2, B3
>>> print(selection.to_dict())
{'A': [1, 2], 'B': [3]}
"""
def __init__(self, selection: list = None, delim: str = ",", fast: bool = False, from_scorefile: bool = False):
self.residues = parse_selection(selection, delim=delim, fast=fast, from_scorefile=from_scorefile)
def __len__(self) -> int:
return len(self.residues)
def __str__(self) -> str:
return ", ".join([f"{chain}{str(resi)}" for chain, resi in self])
def __iter__(self):
return iter(self.residues)
def __add__(self, other):
if isinstance(other, ResidueSelection):
return ResidueSelection(self.residues + (other - self).residues, fast=True)
return NotImplemented
def __sub__(self, other):
if isinstance(other, ResidueSelection):
return ResidueSelection(tuple(res for res in self.residues if res not in set(other.residues)), fast=True)
return NotImplemented
####################################### INPUT ##############################################
[docs]
def from_selection(self, selection) -> "ResidueSelection":
"""
Constructs a ResidueSelection instance from the provided selection.
Parameters
----------
selection : list or str
The selection of residues to be parsed.
Returns
-------
ResidueSelection
A new ResidueSelection instance.
"""
return residue_selection(selection)
####################################### OUTPUT #############################################
[docs]
def to_string(self, delim: str = ",", ordering: str = None) -> str:
"""
Converts the ResidueSelection to a string.
Parameters
----------
delim : str, optional
The delimiter to use in the resulting string. Default is ",".
ordering : str, optional
Specifies the ordering of the residues in the output string. Options are "rosetta" or "pymol".
Default is None.
Returns
-------
str
ResidueSelection object formatted as a string, separated by :delim:
ueSelection.
Examples
--------
>>> selection = ResidueSelection(["A1", "A2", "B3"])
>>> print(selection.to_string())
A1, A2, B3
>>> print(selection.to_string(ordering="rosetta"))
1A, 2A, 3B
"""
ordering = ordering or ""
if ordering.lower() == "rosetta":
return delim.join([str(idx) + chain for chain, idx in self])
if ordering.lower() == "pymol":
return delim.join([chain + str(idx) for chain, idx in self])
return delim.join([chain + str(idx) for chain, idx in self])
[docs]
def to_list(self, ordering: str = None) -> list[str]:
"""
Converts the ResidueSelection to a list of strings.
Parameters
----------
ordering : str, optional
Specifies the ordering of the residues in the output list. Options are "rosetta" or "pymol".
Default is None.
Returns
-------
list of str
The list representation of the ResidueSelection.
Examples
--------
>>> selection = ResidueSelection(["A1", "A2", "B3"])
>>> print(selection.to_list())
['A1', 'A2', 'B3']
>>> print(selection.to_list(ordering="rosetta"))
['1A', '2A', '3B']
"""
ordering = ordering or ""
if ordering.lower() == "rosetta":
return [str(idx) + chain for chain, idx in self]
if ordering.lower() == "pymol":
return [chain + str(idx) for chain, idx in self]
return [chain+str(idx) for chain, idx in self]
[docs]
def to_dict(self) -> dict:
"""
Converts the ResidueSelection to a dictionary.
Note
----
Converting to a dictionary destroys the ordering of specific residues on the same chain in a motif.
Returns
-------
dict
A dictionary representation of the ResidueSelection with chains as keys and lists of residue
indices as values.
Examples
--------
>>> selection = ResidueSelection(["A1", "A2", "B3"])
>>> print(selection.to_dict())
{'A': [1, 2], 'B': [3]}
"""
# collect list of chains and setup chains as dictionary keys
chains = list(set([x[0] for x in self.residues]))
out_d = {chain: [] for chain in chains}
# aggregate all residues to the chains and return
for (chain, res_id) in self.residues:
out_d[chain].append(res_id)
return out_d
[docs]
def to_rfdiffusion_contig(self) -> str:
"""
Parses ResidueSelection object to contig string for RFdiffusion.
Example:
If self.residues = (("A", 1), ("A", 2), ("A", 3), ("C", 4), ("C", 6)),
the output will be "A1-3,C4,C6".
"""
# Collect residues per chain
chain_residues = defaultdict(list)
for chain, resnum in self.residues:
chain_residues[chain].append(resnum)
contig_parts = []
# Process each chain separately
for chain in sorted(chain_residues.keys()):
# Sort residue numbers for the chain
resnums = sorted(chain_residues[chain])
# Find consecutive ranges
ranges = []
start = prev = resnums[0]
for resnum in resnums[1:]:
if resnum == prev + 1:
# Continue the consecutive range
prev = resnum
else:
# End of the current range
if start == prev:
# Single residue
ranges.append(f"{chain}{start}")
else:
# Range of residues
ranges.append(f"{chain}{start}-{prev}")
# Start a new range
start = prev = resnum
# Add the last range
if start == prev:
ranges.append(f"{chain}{start}")
else:
ranges.append(f"{chain}{start}-{prev}")
# Add ranges to the contig parts
contig_parts.extend(ranges)
# Combine all parts into the final contig string
contig_str = ",".join(contig_parts)
return contig_str
[docs]
def fast_parse_selection(input_selection: tuple[tuple[str, int]]) -> tuple[tuple[str, int]]:
"""
Fast selection parser for pre-formatted selections.
This function is a fast parser for residue selections that are already in the `ResidueSelection` format.
It bypasses any additional type checking or parsing to improve performance when the input is guaranteed
to be correctly formatted.
Parameters
----------
input_selection : tuple of tuple of (str, int)
A tuple of tuples where each inner tuple represents a residue with the format (chain, residue_index).
Returns
-------
tuple of tuple of (str, int)
The input selection, unchanged.
Examples
--------
>>> input_selection = (("A", 1), ("B", 2), ("C", 3))
>>> fast_parse_selection(input_selection)
(('A', 1), ('B', 2), ('C', 3))
"""
return input_selection
[docs]
def parse_from_scorefile(input_selection: dict) -> tuple[tuple[str, int]]:
if isinstance(input_selection, dict) and "residues" in input_selection:
return tuple([tuple(sele) for sele in input_selection["residues"]])
else:
raise TypeError(f"Unsupported Input type for parameter 'input_selection' {type(input_selection)}. This function is meant to parse ResidueSelections that were written to file. Only dict with 'residues' as key allowed.")
[docs]
def parse_selection(input_selection, delim: str = ",", fast: bool = False, from_scorefile: bool = False) -> tuple[tuple[str,int]]:
"""
Parses a selection into ResidueSelection formatted selection.
This function takes a selection of residues in various formats and parses it into the `ResidueSelection`
format, which is a tuple of tuples. Each inner tuple represents a residue with the format (chain, residue_index).
Parameters
----------
input_selection : str, list, or tuple
The selection of residues to be parsed. This can be:
- A string with residues separated by a delimiter.
- A list or tuple of residue strings.
- A list or tuple of lists/tuples, where each inner list/tuple represents a residue.
delim : str, optional
The delimiter used to split the input string if `input_selection` is a string. Default is ",".
fast : bool, optional
If True, uses `fast_parse_selection` to bypass type checking and parsing for performance reasons.
Use when `input_selection` is already in the correct format. Default is False.
from_scorefile : bool, optional
If True, parses a residue selection that was read in from a scorefile (in the form {'residues': [['A', 1], ['B', 3]}).
Default is False.
Returns
-------
tuple of tuple of (str, int)
A tuple of tuples where each inner tuple represents a residue in the format (chain, residue_index).
Raises
------
TypeError
If `input_selection` is not a supported type (str, list, or tuple).
Examples
--------
>>> parse_selection("A1, B2, C3")
(('A', 1), ('B', 2), ('C', 3))
>>> parse_selection(["A1", "B2", "C3"])
(('A', 1), ('B', 2), ('C', 3))
>>> parse_selection([["A", 1], ["B", 2], ["C", 3]])
(('A', 1), ('B', 2), ('C', 3))
>>> parse_selection([("A", 1), ("B", 2), ("C", 3)], fast=True)
(('A', 1), ('B', 2), ('C', 3))
"""
if fast and from_scorefile:
raise RuntimeError(":fast: and :from_scorefile: are mutually exclusive!")
if fast:
return fast_parse_selection(input_selection)
if from_scorefile:
return parse_from_scorefile(input_selection)
if isinstance(input_selection, str):
return tuple(parse_residue(residue.strip()) for residue in input_selection.split(delim))
if isinstance(input_selection, (list, tuple)):
if all(isinstance(residue, str) for residue in input_selection):
return tuple(parse_residue(residue) for residue in input_selection)
if all(isinstance(residue, (list, tuple)) for residue in input_selection):
return tuple(parse_residue("".join([str(r) for r in residue])) for residue in input_selection)
raise TypeError(f"Unsupported Input type for parameter 'input_selection' {type(input_selection)}. Only str and list allowed.")
[docs]
def parse_residue(residue_identifier: str) -> tuple[str,int]:
"""
Parses a single residue identifier into a tuple (chain, residue_index).
This function takes a residue identifier string and parses it into a tuple containing the chain identifier
and the residue index. It currently only supports single-letter chain identifiers.
Parameters
----------
residue_identifier : str
A string representing the residue identifier. The format is expected to be either "chain+residue_index"
or "residue_index+chain", where "chain" is a single letter and "residue_index" is an integer.
Returns
-------
tuple of (str, int)
A tuple containing the chain identifier and the residue index.
Examples
--------
>>> parse_residue("A123")
('A', 123)
>>> parse_residue("123A")
('A', 123)
Notes
-----
- The function determines whether the chain identifier is at the beginning or the end of the string based
on whether the first character is a digit.
- Only single-letter chain identifiers are supported.
"""
chain_first = not residue_identifier[0].isdigit()
# assemble residue tuple
chain = residue_identifier[0] if chain_first else residue_identifier[-1]
residue_index = residue_identifier[1:] if chain_first else residue_identifier[:-1]
# Convert residue_index to int for accurate typing
return (chain, int(residue_index))
[docs]
def residue_selection(input_selection, delim: str = ",") -> ResidueSelection:
"""
Creates a ResidueSelection from a selection of residues.
This function takes an input selection of residues in various formats and creates a `ResidueSelection`
object. The selection can be provided as a string, list, or tuple.
Parameters
----------
input_selection : str, list, or tuple
The selection of residues to be parsed. This can be:
- A string with residues separated by a delimiter.
- A list or tuple of residue strings.
- A list or tuple of lists/tuples, where each inner list/tuple represents a residue.
delim : str, optional
The delimiter used to split the input string if `input_selection` is a string. Default is ",".
Returns
-------
ResidueSelection
An instance of the `ResidueSelection` class representing the parsed selection of residues.
Examples
--------
>>> residue_selection("A1, B2, C3")
<ResidueSelection object representing ('A', 1), ('B', 2), ('C', 3)>
>>> residue_selection(["A1", "B2", "C3"])
<ResidueSelection object representing ('A', 1), ('B', 2), ('C', 3)>
>>> residue_selection([["A", 1], ["B", 2], ["C", 3]])
<ResidueSelection object representing ('A', 1), ('B', 2), ('C', 3)>
"""
return ResidueSelection(input_selection, delim=delim)
[docs]
def from_dict(input_dict: dict) -> ResidueSelection:
"""
Creates a ResidueSelection object from a dictionary.
This function constructs a `ResidueSelection` instance from a dictionary where the keys represent
chain identifiers and the values are lists of residue indices. This format specifies a motif in the
following way: {chain: [residues], ...}.
Parameters
----------
input_dict : dict
A dictionary specifying the motif. The keys are chain identifiers (str) and the values are lists
of residue indices (int).
Returns
-------
ResidueSelection
An instance of the `ResidueSelection` class representing the parsed selection of residues.
Examples
--------
>>> input_dict = {"A": [1, 2], "B": [3, 4]}
>>> from_dict(input_dict)
<ResidueSelection object representing ('A', 1), ('A', 2), ('B', 3), ('B', 4)>
"""
return ResidueSelection([f"{chain}{resi}" for chain, res_l in input_dict.items() for resi in res_l])
[docs]
def from_contig(input_contig: str) -> ResidueSelection:
"""
Creates a ResidueSelection object from a contig string.
This function constructs a `ResidueSelection` instance from a contig string. The contig string can specify
ranges of residues using a hyphen (-) to denote the range, with residues separated by commas (,). For example,
"A1-A3, B5" specifies residues A1, A2, A3, and B5.
Parameters
----------
input_contig : str
A contig string specifying the residues. Ranges can be denoted using hyphens, and residues are separated
by commas.
Returns
-------
ResidueSelection
An instance of the `ResidueSelection` class representing the parsed selection of residues.
Examples
--------
>>> from_contig("A1-A3, B5")
<ResidueSelection object representing ('A', 1), ('A', 2), ('A', 3), ('B', 5)>
>>> from_contig("C1, C3-C5, D2")
<ResidueSelection object representing ('C', 1), ('C', 3), ('C', 4), ('C', 5), ('D', 2)>
"""
sel = []
elements = [x.strip() for x in input_contig.split(",") if x]
for element in elements:
subsplit = element.split("-")
if len(subsplit) > 1:
sel += [element[0] + str(i) for i in range(int(subsplit[0][1:]), int(subsplit[-1])+1)]
else:
sel.append(element)
return ResidueSelection(sel)
[docs]
def reduce_to_unique(input_array: list|tuple) -> list|tuple:
"""
Reduces an input array to its unique elements while preserving order.
This function takes a list or tuple and returns a new list or tuple containing only the unique elements
from the input, with their original order preserved. The type of the returned collection matches the type
of the input.
Parameters
----------
input_array : list or tuple
The input array from which to remove duplicate elements. The order of the elements is preserved.
Returns
-------
list or tuple
A new list or tuple containing only the unique elements from the input array, with the original order
preserved.
Examples
--------
>>> reduce_to_unique([1, 2, 2, 3, 1])
[1, 2, 3]
>>> reduce_to_unique(("a", "b", "a", "c", "b"))
('a', 'b', 'c')
Notes
-----
- The function uses `OrderedDict.fromkeys` to remove duplicates while preserving order.
- The returned collection is of the same type as the input (list or tuple).
"""
return type(input_array)(OrderedDict.fromkeys(input_array))