Quantifying monomer/dimer equilibrium

This notebook shows how to analyse several mass photometry measurements to quantify the equilibrium dissociation constant

The notebook is based on the following publication:

Fineberg, A., Surrey, T., & Kukura, P. (2020). Quantifying the monomer–dimer equilibrium of tubulin with mass photometry. Journal of molecular biology, 432(23), 6168-6172.

First we simulate the measurements and then we fit them.

[1]:
from scripts import create_notebook_6_files, display_figure_static
create_notebook_6_files(
    Kdim=8.35e-9,
    monomer_mass=80,
    concentrations_nM=[1,2,4,8,16,32,64])
# This function creates the necessary files for the notebook
# It simulates mass photometry data for different total monomer concentrations
# The Kd value is set to 8.35 nM

# The simulated concentrations include a random error of 3% to simulate experimental conditions

Generated file: test_files/masses_monomer_1nM.csv
Generated file: test_files/masses_monomer_2nM.csv
Generated file: test_files/masses_monomer_4nM.csv
Generated file: test_files/masses_monomer_8nM.csv
Generated file: test_files/masses_monomer_16nM.csv
Generated file: test_files/masses_monomer_32nM.csv
Generated file: test_files/masses_monomer_64nM.csv
[2]:
import os
import glob
import numpy as np
from pyphotomol import (
    MPAnalyzer,
    plot_histogram,
    plot_histograms_and_fits,
    LayoutConfig,
    LegendConfig
)

mp = MPAnalyzer()

# Get all monomer files with concentrations in filename, sorted by concentration
files = sorted(glob.glob("test_files/masses_monomer_*nM.csv"),
                      key=lambda x: float(x.split('_')[-1].replace('nM.csv', '')))

# Extract the total monomer concentration from each filename
concentrations = [float(f.split('_')[-1].replace('nM.csv', '')) for f in files]
# Convert to string and include 'nM' suffix
concentration_strings = [f"{c} nM" for c in concentrations]

mp.import_files(files,names=concentration_strings)
[3]:
mp.apply_to_all(
    'create_histogram',
    window = [0,1000],
    bin_width = 8
)

layout_config = LayoutConfig(
    stacked=True,  # One plot per file
    show_subplot_titles=True,  # Hide subplot titles
    vertical_spacing=0.06,  # Vertical spacing between subplots
    extra_padding_y_label=0.03  # Extra padding for y-axis label to avoid overlap with axis ticks
)

fig = plot_histogram(mp, layout_config=layout_config)
display_figure_static(fig)
../_images/notebooks_6_quantifying_monomer_dimer_equilibrium_5_0.png
[4]:
# Fit the multigaussian model to the histogram
mp.apply_to_all(
    'fit_histogram',
    peaks_guess=[70,160],
    mean_tolerance=40,
    std_tolerance=50,
    baseline=0
)
[5]:
# Plot the fittings
fig = plot_histograms_and_fits(
    mp,
    layout_config=layout_config
)
display_figure_static(fig)
../_images/notebooks_6_quantifying_monomer_dimer_equilibrium_7_0.png
[6]:
# See the number of counts for one of the files
first_model = list(mp.models.keys())[0]

print(mp.models[first_model].fit_table[['Position / kDa', 'Counts']])
   Position / kDa       Counts
0       79.132374  1623.366652
1      154.668299   188.789717
[7]:
# Extract the counts for all models
counts_monomers = []
counts_dimers   = []

total_monomer_concentration = np.array(concentrations)  # Convert to numpy array

for model_name in list(mp.models.keys()):
    model = mp.models[model_name]
    counts_monomers.append(model.fit_table['Counts'].iloc[0])
    counts_dimers.append(model.fit_table['Counts'].iloc[1])

counts_monomers = np.array(counts_monomers)
counts_dimers   = np.array(counts_dimers)
[8]:

factor_dimer = counts_dimers * 2 factor_monomer = counts_monomers * 1 sum_factors = factor_dimer + factor_monomer concentration_monomer = counts_monomers * total_monomer_concentration / sum_factors concentration_dimer = counts_dimers * total_monomer_concentration / sum_factors # Verify that the calculated concentrations are correct - the division below should yield 1 (concentration_monomer + concentration_dimer * 2) / total_monomer_concentration
[8]:
array([1., 1., 1., 1., 1., 1., 1.])
[9]:
concentration_dimer
[9]:
array([ 0.09435023,  0.26724217,  0.74627812,  1.94430291,  4.74954735,
       11.24103533, 24.80362048])
[10]:

import matplotlib.pyplot as plt plt.figure(figsize=(8, 4)) plt.scatter(total_monomer_concentration, concentration_dimer, marker='o', linestyle='-', color='blue') plt.xlabel('Total Monomer Concentration (nM)') plt.ylabel('Dimer Concentration (nM)') plt.title('Dimer Concentration vs Total Monomer Concentration') plt.grid(True) plt.xticks(total_monomer_concentration) plt.tight_layout() # log scale for x-axis plt.xscale('log') plt.show()
../_images/notebooks_6_quantifying_monomer_dimer_equilibrium_12_0.png
[11]:
dimer_fraction = concentration_dimer * 2 / (concentration_dimer * 2 + concentration_monomer)

plt.figure(figsize=(8, 4))
plt.scatter(total_monomer_concentration, dimer_fraction, marker='o', linestyle='-', color='blue')
plt.xlabel('Total Monomer Concentration (nM)')
plt.ylabel('Dimer Fraction')
plt.title('Dimer Fraction vs Total Monomer Concentration')
plt.grid(True)
plt.xticks(total_monomer_concentration)
plt.tight_layout()
plt.show()
../_images/notebooks_6_quantifying_monomer_dimer_equilibrium_13_0.png
[12]:
from scipy.optimize import curve_fit

# Create a function to fit the dimer fraction and estimate Kd
def fx_dimer_fraction(total_monomer_concentration,Kd):

    free_monomer = (- Kd + np.sqrt(Kd**2 + 8*total_monomer_concentration * Kd)) / 4

    dimer_conc   = (total_monomer_concentration - free_monomer) / 2

    dimer_fraction   = dimer_conc*2  / (total_monomer_concentration)

    return dimer_fraction

params, cov = curve_fit(
    fx_dimer_fraction,
    xdata=total_monomer_concentration,
    ydata=dimer_fraction,
    p0=[1],
    bounds=(0, np.inf)
)

# Fitted Kd value in nM units:
print(f"Fitted Kd: {params[0]:.2f} nM")

Fitted Kd: 8.27 nM
[13]:
# Plot the fitting
plt.figure(figsize=(8, 4))
plt.scatter(total_monomer_concentration, dimer_fraction, marker='o', color='blue', label='Data')
plt.plot(total_monomer_concentration, fx_dimer_fraction(total_monomer_concentration, params[0]), color='red', label='Fitted Curve')
plt.xlabel('Total Monomer Concentration (nM)')
plt.ylabel('Dimer Fraction')
plt.title('Dimer Fraction vs Total Monomer Concentration')
plt.legend()
plt.grid(True)
plt.xticks(total_monomer_concentration)
plt.tight_layout()
plt.show()
# --- IGNORE ---
../_images/notebooks_6_quantifying_monomer_dimer_equilibrium_15_0.png