DNA length in crystal structures
Introduction
A few months ago, a colleague trying to crystallize a protein/DNA complex asked for my input about the length of the DNA molecule to use to make this complex. It is known that the length and type of ends (blunt or cohesive with different numbers of overhanging nucleotides) of a DNA molecule influences the crystallization propensity of a protein/DNA complex (see for instance Hollis, 2007). His approach was to look up a few crystal structures of protein/DNA complexes in the PDB to get a sense of the typical length of DNA in such structures. It is a valid approach, but is nonetheless only anecdotal evidence if it’s based on a small number of randomly chosen protein/DNA complexes. Using programmatic access to the PDB metadata, as I described in a previous post, we can answer such a question on the basis of all deposited crystal structures of protein/DNA complexes. This will give a much finer answer in the form of a distribution of DNA lengths, instead of a guess from a few structures.
Required packages
We will need the following packages:
library(magrittr)
library(curl)
library(jsonlite)
library(tibble)
library(stringr)
library(dplyr)
library(ggplot2)
library(plotly)
Getting data
Building a query
To answer our question, we need to retrieve all molecules matching the following criteria:
- the molecule is DNA,
- the the molecule is found in a structure of a protein/DNA complex,
- the molecule is found in a crystal structure (cryoEM and NMR structures won’t help us make choices for a crystallization project).
This translates as follows in terms of a search query for the PDBe API:
q=molecule_type:"DNA" AND assembly_composition:"DNA/protein complex" AND experimental_method:"X-ray diffraction"
Additionally, we need:
- the sequence of the macromolecule (this is required to answer our question, as we will compute the length from the sequence),
- the PDB accession codes (simply to check which complex contains any given DNA molecule we will find in the results, out of curiosity),
- all results (not only the first 10),
- and we need results in JSON format.
This translates as follows:
fl=pdb_id,molecule_sequence&rows=1000000&wt=json
Putting it together with the base URL of the search API, this gives the following complete URL:
<- 'https://www.ebi.ac.uk/pdbe/search/pdb/select?q=molecule_type:%22DNA%22%20AND%20assembly_composition:%22DNA/protein%20complex%22%20AND%20experimental_method:%22X-ray%20diffraction%22&fl=pdb_id,molecule_sequence&rows=1000000&wt=json' pdb_query
Quoting the spaces in “DNA/protein complex” and “X-ray diffraction” is not sufficient to make the query work properly. Doing so returns an HTTP error 505, while using the URL as it is written above works fine (copying the URL provided by the PDBe API query builder should give the correct escape characters).
Retrieving data
To avoid downloading a new dataset everytime I rebuild this blog, I will store it and retrieve it only if the file doesn’t exist:
<- "datasets/dna-length.json"
dna_length_dataset if (!file.exists(dna_length_dataset)) {
curl_download(url = pdb_query, destfile = dna_length_dataset)
}<- fromJSON(dna_length_dataset) pdb_data
If you try to run the code presented in this post, you will likely get slightly different results as new strutures are deposited in the PDB. To get the same results as in this post, use the dataset saved at the time I last ran this code.
Cleaning data
As explained in the first post in this series, each result
is a macromolecule from the biological assembly (i.e. without crystallographic
duplicates). This is convenient in this case: we received the field
molecule_type
, and the relevant data is already stored in a table, so we can
very easily compute the length of each DNA sequence and store it in a new column
in the same table:
<- pdb_data$response$docs %>%
cleaned_data as_tibble() %>%
mutate(dna_length = str_length(molecule_sequence)) %>%
select(pdb_id, dna_length, dna_sequence = molecule_sequence)
Answering our question
Summary and initial observations
We now have a table of DNA sequences found in crystal structures of protein/DNA complexes, along with their length:
cleaned_data
## # A tibble: 8,221 x 3
## pdb_id dna_length dna_sequence
## <chr> <int> <chr>
## 1 4j8v 145 ATCAATATCCACCTGCAGATACTACCAAAAGTGTATTTGGAAACTGCTCCATCAAAAG…
## 2 4fjn 16 AAGTAAGCAGTCCGCG
## 3 4ail 11 ACGGGTAAGCA
## 4 2w7o 13 GGGGGAAGGATTC
## 5 2w7o 18 TCACGGAATCCTTCCCCC
## 6 4r8a 21 GAATGTGTGTCTCAATCCCAA
## 7 2wtu 16 AGCTGCCAAGCACCAG
## 8 2wtu 16 CTGGTGCATGGCAGCT
## 9 1zns 12 CGGGATATCCCG
## 10 3lja 147 ATCAATATCCACCTGCAGATACTACCAAAAGTGTATTTGGAAACTGCTCCATCAAAAG…
## # … with 8,211 more rows
From this, we can first determine the minimal, maximal, median and average DNA length found in deposited crystal structures of protein/DNA complexes:
summary(cleaned_data$dna_length)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1.00 11.00 14.00 19.54 19.00 1122.00
The minimal length comes as a surprise: a single base pair? It might be an annotation mistake, calling a DNA molecule 1 bp long what might actually be a nucleotide cofactor, and calling a protein/DNA complex what might actually be an enzyme with such a cofactor. There is 1 DNA molecule of this length in our current dataset:
%>% filter(dna_length == min(.$dna_length)) cleaned_data
## # A tibble: 1 x 3
## pdb_id dna_length dna_sequence
## <chr> <int> <chr>
## 1 1mvm 1 A
Turns out it is not an annotation error. PDB 1MVM is a protein/DNA complex.
The maximal length is also a little bit surprising, considering that longer DNA molecules are more flexible, and that flexibility tends to hinder crystallization. There are 2 DNA molecules of this length in our current dataset:
%>% filter(dna_length == max(.$dna_length)) cleaned_data
## # A tibble: 2 x 3
## pdb_id dna_length dna_sequence
## <chr> <int> <chr>
## 1 6hkt 1122 ATCGCTGTTCAATACATGCACAGGATGTATATATCTGACACGTGCCTGGAGACTAGGGA…
## 2 6hkt 1122 ATCACCCTATACGCGGCCGCCCTGGAGAATCCCGGTGCCGAGGCCGCTCAATTGGTCGT…
If I had to guess which structure this is, I would say the one of a nucleosome array. PDB 6HKT is indeed a nucleosome array: 6 nucleosomes bound by linker histone H1.
DNA length distribution
Back to our question: we want a distribution, therefore our answer is best expressed by a histogram showing the number of deposited crystal structures of protein/DNA complexes as a function of the DNA length found in these structures.
The histogram below is interactive, you can zoom in on a region of interest:
<- ggplot(data = cleaned_data) +
final_plot geom_histogram(mapping = aes(x = dna_length), binwidth = 1) +
theme_bw() +
xlab("DNA length (bp)") +
ylab("Crystal structures of protein/DNA complexes")
ggplotly(final_plot)
Most of the distribution resides in a shorter length range, between 0 and 150 bp. The spike at around 147 bp comes from nucleosome structures (mono-nucleosomes, not arrays). It is impressive to see that there are enough of them to stand out significantly in the entire distribution.
Distribution in the 0-50 bp range
Nucleosomes are fascinating, but are admittedly peculiar structures: when designing a piece of DNA for crystallization of a protein/DNA complex other than a nucleosome complex, one should not be biased by the length of nucleosomal DNA. Which means we can further zoom in between 0 and 50 bp and get a clearer picture answering our initial question (median DNA length depicted by a vertical red line):
ggplot(data = cleaned_data, aes(x = dna_length)) +
geom_histogram(binwidth = 1, color = "black", fill = "white") +
geom_vline(xintercept = median(cleaned_data$dna_length), color = "red") +
xlim(c(0, 50)) +
theme_bw() +
xlab("DNA length (bp)") +
ylab("Crystal structures of protein/DNA complexes")
The most common DNA length seems to be 16 bp, or 12 bp if we consider the 16 bp spike an outlier in the distribution.
We can also filter out everything longer than 50 bp and recalculate a less skewed average length:
%>%
cleaned_data filter(dna_length <= 50) %>%
select(dna_length) %>%
summary()
## dna_length
## Min. : 1.00
## 1st Qu.:10.00
## Median :14.00
## Mean :15.15
## 3rd Qu.:18.00
## Max. :50.00
Follow-up questions
This quick analysis suggests at least the following questions:
- What is the diversity of structures with one given DNA length? Around 147 bp in length, no doubt all structures contain a nucleosome. What about spikes in the distribution like those at 5 bp and 35 bp? Are they many related structures? (same DNA sequence, different variants of the binding protein?).
- How does the distribution compare between structures solved by crystallography and cryoEM? My guess here is that the distribution of cryoEM structures across DNA length might be centered on a much longer length, possibly on nucleosomal DNA length (i.e. around 147 bp).
- How do the crystallography and cryoEM distributions compare to the entire PDB distribution? Do they recapitulate the trend over the entire PDB, or are there enough NMR structures to significantly shape the global distribution as well?
- DNA has also been studied in isolation (without any protein bound): what do the 4 distributions (crystallography, NMR, cryoEM and global) look like? My guess here is that there probably isn’t any cryoEM structure of naked DNA, and there is also probably a large number of NMR structures of short sequences.
- What do the equivalent distributions look like for protein/RNA complexes? For isolated RNA structures? How much do ribosome structures skew these distributions?
I might cover some of them in future blog posts.
Replicate this post
You can replicate this post by running the Rmd source with RStudio and R. The easiest way to replicate this post is to clone the entire git repository.
::session_info() sessioninfo
## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.0.3 (2020-10-10)
## os macOS Catalina 10.15.7
## system x86_64, darwin17.0
## ui X11
## language (EN)
## collate en_US.UTF-8
## ctype en_US.UTF-8
## tz Europe/Stockholm
## date 2021-02-15
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date lib source
## assertthat 0.2.1 2019-03-21 [1] CRAN (R 4.0.0)
## blogdown 1.1 2021-01-19 [1] CRAN (R 4.0.3)
## bookdown 0.21 2020-10-13 [1] CRAN (R 4.0.3)
## cli 2.3.0 2021-01-31 [1] CRAN (R 4.0.2)
## colorspace 2.0-0 2020-11-11 [1] CRAN (R 4.0.2)
## crayon 1.4.1 2021-02-08 [1] CRAN (R 4.0.3)
## crosstalk 1.1.1 2021-01-12 [1] CRAN (R 4.0.2)
## curl * 4.3 2019-12-02 [1] CRAN (R 4.0.0)
## data.table 1.13.6 2020-12-30 [1] CRAN (R 4.0.2)
## DBI 1.1.1 2021-01-15 [1] CRAN (R 4.0.3)
## digest 0.6.27 2020-10-24 [1] CRAN (R 4.0.2)
## dplyr * 1.0.4 2021-02-02 [1] CRAN (R 4.0.2)
## ellipsis 0.3.1 2020-05-15 [1] CRAN (R 4.0.0)
## evaluate 0.14 2019-05-28 [1] CRAN (R 4.0.0)
## fansi 0.4.2 2021-01-15 [1] CRAN (R 4.0.2)
## generics 0.1.0 2020-10-31 [1] CRAN (R 4.0.3)
## ggplot2 * 3.3.3 2020-12-30 [1] CRAN (R 4.0.3)
## glue 1.4.2 2020-08-27 [1] CRAN (R 4.0.2)
## gtable 0.3.0 2019-03-25 [1] CRAN (R 4.0.0)
## htmltools 0.5.1.1 2021-01-22 [1] CRAN (R 4.0.2)
## htmlwidgets 1.5.3 2020-12-10 [1] CRAN (R 4.0.2)
## httr 1.4.2 2020-07-20 [1] CRAN (R 4.0.2)
## jsonlite * 1.7.2 2020-12-09 [1] CRAN (R 4.0.2)
## knitr 1.31 2021-01-27 [1] CRAN (R 4.0.2)
## labeling 0.4.2 2020-10-20 [1] CRAN (R 4.0.3)
## lazyeval 0.2.2 2019-03-15 [1] CRAN (R 4.0.0)
## lifecycle 0.2.0 2020-03-06 [1] CRAN (R 4.0.0)
## magrittr * 2.0.1 2020-11-17 [1] CRAN (R 4.0.2)
## munsell 0.5.0 2018-06-12 [1] CRAN (R 4.0.0)
## pillar 1.4.7 2020-11-20 [1] CRAN (R 4.0.2)
## pkgconfig 2.0.3 2019-09-22 [1] CRAN (R 4.0.0)
## plotly * 4.9.3 2021-01-10 [1] CRAN (R 4.0.2)
## purrr 0.3.4 2020-04-17 [1] CRAN (R 4.0.0)
## R6 2.5.0 2020-10-28 [1] CRAN (R 4.0.2)
## rlang 0.4.10 2020-12-30 [1] CRAN (R 4.0.2)
## rmarkdown 2.6 2020-12-14 [1] CRAN (R 4.0.3)
## scales 1.1.1 2020-05-11 [1] CRAN (R 4.0.0)
## sessioninfo 1.1.1 2018-11-05 [1] CRAN (R 4.0.0)
## stringi 1.5.3 2020-09-09 [1] CRAN (R 4.0.2)
## stringr * 1.4.0 2019-02-10 [1] CRAN (R 4.0.0)
## tibble * 3.0.6 2021-01-29 [1] CRAN (R 4.0.2)
## tidyr 1.1.2 2020-08-27 [1] CRAN (R 4.0.2)
## tidyselect 1.1.0 2020-05-11 [1] CRAN (R 4.0.0)
## utf8 1.1.4 2018-05-24 [1] CRAN (R 4.0.0)
## vctrs 0.3.6 2020-12-17 [1] CRAN (R 4.0.2)
## viridisLite 0.3.0 2018-02-01 [1] CRAN (R 4.0.0)
## withr 2.4.1 2021-01-26 [1] CRAN (R 4.0.3)
## xfun 0.21 2021-02-10 [1] CRAN (R 4.0.2)
## yaml 2.2.1 2020-02-01 [1] CRAN (R 4.0.0)
##
## [1] /Users/guillaume/Library/R/4.0/library
## [2] /Library/Frameworks/R.framework/Versions/4.0/Resources/library