Compute a scattering potential
This tutorial demonstrates how to use cryojax
to build a scattering potential from a PDB entry. Specifically, the tutorial will build a voxel grid, take some steps to validate it, then save the result to be used in subsequent tutorials.
The scattering potential will be built using the tabulation of atomic scattering factors from the work of Lian-Mao Peng, which fits the potential from single atoms to a sum of five gaussians. See the cryojax
documentation here for more information.
References:
- Peng, L-M., et al. "Robust parameterization of elastic and absorptive electron atomic scattering factors." Acta Crystallographica Section A: Foundations of Crystallography 52.2 (1996): 257-276.
- Himes, Benjamin, and Nikolaus Grigorieff. "Cryo-TEM simulations of amorphous radiation-sensitive samples using multislice wave propagation." IUCrJ 8.6 (2021): 943-953.
# Plotting imports and function definitions
from matplotlib import pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
def plot_image(image, fig, ax, cmap="gray", label=None, **kwargs):
im = ax.imshow(image, cmap=cmap, origin="lower", **kwargs)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
fig.colorbar(im, cax=cax)
if label is not None:
ax.set(title=label)
return fig, ax
First, load the atomic positions and identities from a PDB entry. Here, a structure of GroEL (PDB ID 5w0s) is used. This is loaded into a PengAtomicPotential
object.
import cryojax.simulator as cxs
from cryojax.io import read_atoms_from_pdb
atom_positions, atom_identities = read_atoms_from_pdb("./data/5w0s.pdb")
atom_potential = cxs.PengAtomicPotential(atom_positions, atom_identities)
print(atom_potential)
We see above that the PengAtomicPotential
consists of three fields: the atom_positions
, scattering_factor_a
, and scattering_factor_b
.
- The
atom_positions
are the positions of the atoms in angstroms. - The
scattering_factor_a
, andscattering_factor_b
are the parameters \(a_i\) and \(b_i\) from Peng et al. (1996).
Optionally, we can also load PDB B-factors into the b_factors
field after making the function call cryojax.io.read_atoms_from_pdb(..., get_b_factors=True)
.
Next, we can build the voxel grid representation of the potential.
# Evaluate the potential on a voxel grid
shape = (240, 240, 240)
voxel_size = 1.0
real_voxel_grid = atom_potential.as_real_voxel_grid(
shape,
voxel_size,
z_planes_in_parallel=10,
)
Now, downsample the voxel array to the desired voxel size. Because the potentials from individual atoms are short-ranged, finite sampling effects can be significant and it is best to first generate a potential at a smaller-than-desired voxel size.
from cryojax.image import downsample_with_fourier_cropping
downsampling_factor = 3
downsampled_voxel_size = downsampling_factor * voxel_size
downsampled_voxel_grid = downsample_with_fourier_cropping(
real_voxel_grid, downsampling_factor
)
Info
In, cryojax
, potentials are built in units of inverse length squared,
\([L]^{-2}\). This rescaled potential is defined to be
where \(V\) is the electrostatic potential energy, \((x, y, z)\) are positional coordinates, \(m\) is the electron mass, and \(e\) is the electron charge.
In the following, we will compute projections of the potential, which we will define to be
where in practice the integration domain is taken to be between \(z'\)-planes above and below where the potential has sufficiently decayed. In this tutorial, this integral is computed with fourier slice extraction.
Now, let's validate that what we see is reasonable. The first validation step is to compute a few different projections of the potential.
import jax
from cryojax.image import irfftn
@jax.jit
def compute_projection(potential):
"""Compute a projection of a voxel-based potential."""
# ... initialize the integration method for the potential
potential_integrator = cxs.FourierSliceExtraction(interpolation_order=1)
# ... and the configuration of the imaging instrument
instrument_config = cxs.InstrumentConfig(
shape=potential.shape[0:2],
pixel_size=potential.voxel_size,
voltage_in_kilovolts=300.0,
)
# ... compute the integrated potential
fourier_integrated_potential = (
potential_integrator.compute_fourier_integrated_potential(
potential, instrument_config
)
)
return irfftn(fourier_integrated_potential, s=instrument_config.shape)
# Load the voxel grid into a voxel-based potential representation
voxel_potential = cxs.FourierVoxelGridPotential.from_real_voxel_grid(
downsampled_voxel_grid, downsampled_voxel_size
)
# Now, compute the projection integral
integrated_potential = compute_projection(voxel_potential)
# ... and plot
fig, ax = plt.subplots(figsize=(3, 3))
plot_image(integrated_potential, fig, ax, label="Integrated potential, $U_z(x, y)$")
We can also inspect a different viewing angle by rotating the voxel_potential
to a different pose. This involves instantiating a cryojax
representation of a pose, which here is the cryojax.simulator.EulerAnglePose
. The three euler angles in this object are:
- The first euler angle \(\phi\), denoted
view_phi
- The second euler angle \(\theta\), denoted
view_theta
- The third euler angle \(\psi\), denoted
view_psi
The euler angle convention in cryojax
is a zyz extrinsic rotation, which follows other standard cryo-EM software, such as RELION and cisTEM.
# Instantiate the pose and rotate the potential
# ... angles are in degrees
pose = cxs.EulerAnglePose(view_phi=0.0, view_theta=90.0, view_psi=90.0)
rotated_voxel_potential = voxel_potential.rotate_to_pose(pose)
# ... again compute the projection integral
integrated_potential = compute_projection(rotated_voxel_potential)
# ... and again, plot
fig, ax = plt.subplots(figsize=(3, 3))
plot_image(integrated_potential, fig, ax, label="Integrated potential, $U_z(x, y)$")
Another good sanity check is to check that the potential is relatively weak compared to a typical incident electron beam energy in cryo-EM. For an electron beam with incident wavenumber \(k\), this can be checked with the condition
where again \(U = 2 m e V / \hbar^2\) is the rescaled potential. Below, we consider an incident energy of \(300 \ \textrm{keV}\).
import numpy as np
from cryojax.constants import convert_keV_to_angstroms
# First compute the wavenumber
voltage_in_kilovolts = 300.0
wavelength_in_angstroms = convert_keV_to_angstroms(voltage_in_kilovolts)
wavenumber = 2 * np.pi / wavelength_in_angstroms
# ... now get the maximum value of the potential
potential_maximum = downsampled_voxel_grid.max()
# ... and compare
print(potential_maximum / wavenumber**2)
Looks reasonable! Finally, we can write the voxel grid to disk for later processing.
from cryojax.io import write_volume_to_mrc
write_volume_to_mrc(
downsampled_voxel_grid,
downsampled_voxel_size,
"./data/groel_5w0s_scattering_potential.mrc",
)