Came across this looking for a solution myself last night but now I have created my own I will share it.
Rather then using Bio
python (which is a Python package I use a lot), it is more computationally efficient to parse the .fasta
files from the protein data bank (RCSB in this case) which as far as I can see, contain this information. For example, this is 2P16's .fasta
file (with 2 unique chains):
>2P16_1|Chain A|Coagulation factor X (EC 3.4.21.6) (Stuart factor) (Stuart-Prower factor)|Homo sapiens (9606)
IVGGQECKDGECPWQALLINEENEGFCGGTILSEFYILTAAHCLYQAKRFKVRVGDRNTEQEEGGEAVHEVEVVIKHNRFTKETYDFDIAVLRLKTPITFRMNVAPACLPERDWAESTLMTQKTGIVSGFGRTHEKGRQSTRLKMLEVPYVDRNSCKLSSSFIITQNMFCAGYDTKQEDACQGDSGGPHVTRFKDTYFVTGIVSWGEGCARKGKYGIYTKVTAFLKWIDRSMKT
>2P16_2|Chain B[auth L]|Coagulation factor X (EC 3.4.21.6) (Stuart factor) (Stuart-Prower factor)|Homo sapiens (9606)
KLCSLDNGDCDQFCHEEQNSVVCSCARGYTLADNGKACIPTGPYPCGKQTLE
and 6WON's (with 8 of the same chains):
>6WON_1|Chains A, B, C, D, E, F, G, H|YohF|Salmonella typhimurium (strain LT2 / SGSC1412 / ATCC 700720) (99287)
QGMTKVAIVTASDSGIGKACALLLAQNGFDIGITWHSDERGAQETAKKAAQFGVRAETIHLDLSQLPEGAQAIEHLIQRLGRVDVLVNNAGAMTKSAFIDMPFTQWRQIFTVDVDGAFLCAQIAARHMIKQGEGGRIINITSVHEHTPLPQASAYTAAKHALGGLTKSMALELIEHHILVNAVAPGAIATPMNDMDDSDIEPGSEPSIPIARPGSTHEIASLVAWLCSEGASYTTGQSLIVDGGFMLANPQFNAK
So far this always seems to be the case and parsing the .fasta
is a lot easier then using BLAST or sequence similarity such as you are or were.
For the implementation, whereas the .pdb
, .cif
files etc use the same download URL (https://files.rcsb.org/download/
), the .fasta
files use this link: https://www.rcsb.org/fasta/entry/
. Parsing the downloaded .fasta
files and only reading the lines that begin with >
, stripping out unnecessary chains (in my specific case I just want the first unique chains in the PDB so A, B
being the same and C, D
being the same would return me a list with [A, C
], you can modify this script to your own ends) and running the Python code gives this output:
First unique chains in 1d2z (2 unique chains): ['B', 'A']
First unique chains in 2p16 (2 unique chains): ['A', 'B']
First unique chains in 6won (1 unique chain): ['A']
There was an issue with some entries having Chain
or Chains
so both had to be considered. Additionally, there are commas that can be removed from all but the last chain ID but for my purposes I do not need to strip it, you can put something in _fasta_helper()
to this end before return
. This is the Python code and hopefully it helps someone out in the future:
import re
from typing import Union
import urllib.request
from urllib.error import (HTTPError,
URLError,
ContentTooShortError as ContentError)
def rcsb_downloader(pdb: str, ext: str = None, down_path: str = None) -> str:
"""Downloads the PDB from the protein databank RCSB.
Parameters
----------
pdb : str
Protein databank accession 4 letter code i.e. '6won'
ext : str, default = None
Extension of the file to download i.e. '.fasta', '.cif' etc. Note: if
None argument then ext = '.pdb'
Returns
-------
str | bool
Returns str path if download successful, False if failure
"""
if ext is None:
ext = '.pdb'
if down_path is None:
download_path = ('/your/path/to/dir/' + pdb + ext)
else:
download_path = down_path + pdb + ext
rcsb_path = 'https://files.rcsb.org/download/' + pdb + ext
if ext == '.fasta': # fasta has a different web search then others
rcsb_path = 'https://www.rcsb.org/fasta/entry/' + pdb
try:
urllib.request.urlretrieve(rcsb_path, download_path)
return download_path
# Try again in case there was a temporary issue with the first attempt
except (ContentError, HTTPError, URLError):
try:
urllib.request.urlretrieve(rcsb_path, download_path)
return download_path
except (ContentError, HTTPError, URLError):
print(f"PDB failed to download {pdb}")
return False
def _fasta_helper(self, line: str, single_unique_chain: bool = True) -> Union[str, list]:
"""Separate the first chain ID from the .fasta comment line."""
# [1] some of the chains had [auth C] etc after so this is removed
# [2] was originally a list comprehension that filtered based on the word
# chain, however some of the fasta entries had "Chain" or "Chains" in their
# description which broke this. The first arrow is removed, split at |, the
# second entry (1st in Python) entry contains the chain information and the
# first entry removed (which so far has always been chain, anticipate their
# being edge cases with older PDB entries). This appears to hold up.
clean_chain = [
x.replace(",", "")
for x in list(
re.sub(r"\[.*?\]", "", line).lstrip(">").split("|")[1].split(" ")[1:] # [1] # [2]
)
]
if not single_unique_chain:
return clean_chain
return clean_chain[0]
def fasta_parse(path_: str) -> list:
"""Parse .fasta files and analyse lines beginning with '>'."""
with open(path_, 'r', encoding='utf-8') as fasta:
total_unique_chains = 0 # Use as a flag to test if more than one unique chain
unique_chains = []
for line in fasta:
if line[0] == '>' and total_unique_chains == 0:
total_unique_chains += 1
unique_chains.append(_fasta_helper(line))
continue
elif line[0] == '>' and total_unique_chains != 0:
unique_chains.append(_fasta_helper(line))
return unique_chains
pdb_diff_chain = '2p16' # Chain ...
pdb_diff_chains = '1d2z' # Chains ...
pdb_same_chains = '6won' # Chains ...
download_p_diff = rcsb_downloader(pdb_diff_chain, '.fasta')
download_p_diffs = rcsb_downloader(pdb_diff_chains, '.fasta')
download_p_same = rcsb_downloader(pdb_same_chains, '.fasta')
unique_s = fasta_parse(download_p_same)
unique_d = fasta_parse(download_p_diffs)
unique_ds = fasta_parse(download_p_diff)
print(f'First unique chains in {pdb_diff_chains} (2 unique chains): {unique_ds}\n'
f'First unique chains in {pdb_diff_chain} (2 unique chains): {unique_d}\n'
f'First unique chains in {pdb_same_chains} (1 unique chain): {unique_s}')