This page was generated from docs/examples/6_timeresolvedPYP.ipynb. Interactive online version: Binder badge.

Time-Resolved Difference Map

Time-resolved crystallography experiments make use of X-ray diffraction to monitor structural changes in a crystalline sample. Such experiments typically use a pump-probe setup where atomic motions are induced by a “pump” (often some sort of a laser pulse) and then are monitored using a probe X-ray pulse. Due to the need for an X-ray pulse with a short time duration, it is most common to conduct time-resolved diffraction experiments using an XFEL or using a Laue (pink beam) beamline at a synchrotron in order to achieve the desired X-ray intensity.

A common methodology for investigating structural changes in time-resolved experiments is using difference maps between structure factor amplitudes collected with and without a perturbation. These \((|F_{On}| - |F_{Off}|)\) maps may be noisy due to systematic errors or scaling artifacts, and have historically been weighted based on the magnitude of the difference signal and/or the error estimates associated with the measured values.

Photoactive yellow protein (PYP) is a model system in time-resolved crystallography due to the trans-cis isomerization of its chromophore which occurs upon absorption of blue light. Here, we will use reciprocalspaceship to produce a weighted difference map of PYP to investigate the structural changes that occur 2ms after illumination with blue light. This dataset was collected at the BioCARS Laue beamline APS-14-ID from a PYP crystal with \(P 6_3\) symmetry, and were processed using Precognition (Renz Research, Inc.), a software suite for Laue data processing. This data was collected and provided by Marius Schmidt and Vukica Šrajer.

[1]:
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_context("notebook", font_scale=1.4)
import numpy as np
[2]:
import reciprocalspaceship as rs
[3]:
print(rs.__version__)
0.10.3

Load On and Off PYP DataSets

The .hkl files used in this example came directly from Precognition and contain scaled, merged structure amplitudes (F) and associated errors (SigF). Precognition uses a different convention for the reciprocal space asymmetric unit (ASU) than CCP4/Phenix, so we will map all reflections to the ASU.

[4]:
def load(hkl, cell=(66.9, 66.9, 40.8, 90.0, 90.0, 120.0), sg=173):
    """
    Load HKL file from Precognition and map reflections to the reciprocal space ASU.
    """
    dataset = rs.read_precognition(hkl, cell=cell, spacegroup=sg)
    dataset.hkl_to_asu(inplace=True)
    dataset.drop(columns="M/ISYM", inplace=True)
    return dataset
[5]:
off = load("data/PYP_varEll_2sig_off_1.6A.hkl")
on  = load("data/PYP_varEll_2sig_2ms_1.6A.hkl")

Since difference maps can only be made with Fourier magnitudes that were measured in both datasets, we will subset the datasets to their common Miller indices.

[6]:
pyp = off.merge(on, left_index=True, right_index=True, suffixes=("_off", "_on"))
[7]:
pyp.head()
[7]:
F_off SigF_off F_on SigF_on
H K L
0 1 2 56.646 0.036 75.991 0.032
3 89.061 0.03 89.667 0.031
4 95.419 0.048 93.886 0.047
5 160.466 0.038 162.553 0.035
6 77.869 0.075 79.139 0.083

Compute Difference Map Coefficients and Errors

We will compute \((|F_{On}| - |F_{Off}|)\) for use as the coefficients of the difference map, and we will propagate the uncertainties in quadrature. These propagated uncertainties will be used when computing weights for each Miller index.

[8]:
pyp["DF"] = (pyp["F_on"] - pyp["F_off"]).astype("SFAmplitude")
pyp["SigDF"] = np.sqrt(pyp["SigF_on"]**2 + pyp["SigF_off"]**2).astype("Stddev")

Compute Difference Map Weights

There are several weighting schemes that have been used to produce time-resolved difference maps. Many of them take the form below, involving a term based on the uncertainty in the difference structure factor amplitude (\(\sigma_{\Delta F}\)), and optionally, a scale term based on the the magnitude of the observed \(\Delta F\). With \(\alpha=0\), these weights take the form employed in Ursby and Bourgeois, Acta Cryst (1997). On the other hand, Šrajer et al, Biochemistry (2001) employed weights with \(\alpha=1\) in order to decrease the impact of abnormally large values of \(\Delta F\) that may have erroneously small uncertainties. Finally, other weighting schemes have employed intermediate values of \(\alpha\) (Hekstra et al, Nature (2016)).

\begin{equation} w = \left(1 + \frac{\sigma_{\Delta F}^2}{\overline{\sigma_{\Delta F}^2}} + \alpha\frac{|\Delta F|^2}{\overline{ |\Delta F|^2 }} \right)^{-1} \end{equation}

For illustration purposes, we will compute weights with \(\alpha=0.05\).

[9]:
def compute_weights(df, sigdf, alpha=0):
    """
    Compute weights for each structure factor based on deltaF and its uncertainty
    """
    w = (1 + (sigdf**2 / (sigdf**2).mean()) + alpha*(df**2 / (df**2).mean()))
    return w**-1
[10]:
pyp["W"] = compute_weights(pyp["DF"], pyp["SigDF"], alpha=0.05)
pyp["W"] = pyp["W"].astype("W")
pyp["WDF"] = (pyp["W"]*pyp["DF"]).astype("F")

Let’s visualize the weights relative to the magnitude of \(\Delta F\) and the signal-to-noise ratio in order to understand how they will affect different structure factors.

[11]:
fig, ax = plt.subplots(figsize=(10, 7))
pts = ax.scatter(pyp.DF, pyp.DF.abs()/pyp.SigDF, c=pyp.W)
ax.set_xlabel(r"$\Delta F$")
ax.set_ylabel(r"$\frac{\left| \Delta F \right|}{\sigma_{\Delta F}}$")

# Inset
axins = ax.inset_axes([0.6, 0.6, 0.37, 0.37])
axins.scatter(pyp.DF, pyp.DF.abs()/pyp.SigDF, c=pyp.W)
x1, x2, y1, y2 = -25, 25, 0, 20
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
ax.indicate_inset_zoom(axins)

fig.colorbar(pts, label="Weight")
plt.show()
../_images/examples_6_timeresolvedPYP_16_0.png

As seen in the above plot, difference Fourier coefficients with low signal-to-noise ratios (large \(\sigma_{\Delta F}\) relative to \(|\Delta F|\)) are assigned lower weight. Difference Fourier coefficients with large amplitude are also assigned lower weight.


Visualize Weighted Difference Map

We have our difference Fourier amplitudes and weights, so we just need phases in order to make a difference map. We will use phases from a refined “dark” state model of PYP, PDB: 2PHY. Phases were computed using phenix.fmodel.

[12]:
ref = rs.read_mtz("data/2PHY.pdb.mtz")
pyp["PHIFMODEL"] = ref.loc[pyp.index, "PHIFMODEL"]
pyp.write_mtz("data/PYP_diffmap.mtz")

Let’s now visualize the effects of weighting the \((F_{On} - F_{Off})\) difference map. The map is overlaid with the refined model of the dark state (red; PDB: 2PHY) and the pB intermediate state of the chromophore (green; PDB: 3UME).

[13]:
%%html
<center>
    <iframe src="https://rs-station.github.io/reciprocalspaceship/data/pypdiff/comparison.html#xyz=14.870,0.383,-18.885&eye=85.6,69.5,-33.7&zoom=60", width=625, height=300></iframe>
    <br>Time-resolved difference map of PYP photocycle with and without weights. The dark state structure is shown in red, and the pB intermediate state of the chromophore is shown in green.
</center>

Time-resolved difference map of PYP photocycle with and without weights. The dark state structure is shown in red, and the pB intermediate state of the chromophore is shown in green.