Analysing lipid membrane data

This Jupyter notebook demonstrates the utility of the refnx for:

  • the co-refinement of three contrast variation datasets of a DMPC (1,2-dimyristoyl-sn-glycero-3-phosphocholine) bilayer measured at the solid-liquid interface with a common model

  • the use of the LipidLeaflet component to parameterise the model in terms of physically relevant parameters

  • the use of Bayesian Markov Chain Monte Carlo (MCMC) to investigate the Posterior distribution of the curvefitting system.

  • the intrinsic usefulness of Jupyter notebooks to facilitate reproducible research in scientific data analysis

The first step in most Python scripts is to import modules and functions that are going to be used

# use matplotlib for plotting
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import os.path

import refnx, scipy

# the analysis module contains the curvefitting engine
from refnx.analysis import CurveFitter, Objective, Parameter, GlobalObjective, process_chain

# the reflect module contains functionality relevant to reflectometry
from refnx.reflect import SLD, ReflectModel, Structure, LipidLeaflet

# the ReflectDataset object will contain the data
from refnx.dataset import ReflectDataset

In order for the analysis to be exactly reproducible the same package versions must be used. The conda packaging manager, and pip, can be used to ensure this is the case.

# version numbers used in this analysis
refnx.version.version, scipy.version.version
('0.1.6', '1.2.1')

The ReflectDataset class is used to represent a dataset. They can be constructed by supplying a filename

pth = os.path.join(os.path.dirname(refnx.__file__), 'analysis', 'test')

data_d2o = ReflectDataset(os.path.join(pth, 'c_PLP0016596.dat')) = "d2o"

data_hdmix = ReflectDataset(os.path.join(pth, 'c_PLP0016601.dat')) = "hdmix"

data_h2o = ReflectDataset(os.path.join(pth, 'c_PLP0016607.dat')) = "h2o"

A SLD object is used to represent the Scattering Length Density of a material. It has real and imag attributes because the SLD is a complex number, with the imaginary part accounting for absorption. The units of SLD are \(10^{-6} \mathring{A}^{-2}\)

The real and imag attributes are Parameter objects. These Parameter objects contain the: parameter value, whether it allowed to vary, any interparameter constraints, and bounds applied to the parameter. The bounds applied to a parameter are probability distributions which encode the log-prior probability of the parameter having a certain value.

si = SLD(2.07 + 0j)
sio2 = SLD(3.47 + 0j)

# the following represent the solvent contrasts used in the experiment
d2o = SLD(6.36 + 0j)
h2o = SLD(-0.56 + 0j)
hdmix = SLD(2.07 + 0j)

# We want the `real` attribute parameter to vary in the analysis, and we want to apply
# uniform bounds. The `setp` method of a Parameter is a way of changing many aspects of
# Parameter behaviour at once.
d2o.real.setp(vary=True, bounds=(6.1, 6.36))'d2o SLD'

The LipidLeaflet class is used to describe a single lipid leaflet in our interfacial model. A leaflet consists of a head and tail group region. Since we are studying a bilayer then inner and outer LipidLeaflet’s are required.

# Parameter for the area per molecule each DMPC molecule occupies at the surface. We
# use the same area per molecule for the inner and outer leaflets.
apm = Parameter(56, 'area per molecule', vary=True, bounds=(52, 65))

# the sum of scattering lengths for the lipid head and tail in Angstrom.
b_heads = Parameter(6.01e-4, 'b_heads')
b_tails = Parameter(-2.92e-4, 'b_tails')

# the volume occupied by the head and tail groups in cubic Angstrom.
v_heads = Parameter(319, 'v_heads')
v_tails = Parameter(782, 'v_tails')

# the head and tail group thicknesses.
inner_head_thickness = Parameter(9, 'inner_head_thickness', vary=True, bounds=(4, 11))
outer_head_thickness = Parameter(9, 'outer_head_thickness', vary=True, bounds=(4, 11))
tail_thickness = Parameter(14, 'tail_thickness', vary=True, bounds=(10, 17))

# finally construct a `LipidLeaflet` object for the inner and outer leaflets.
# Note that here the inner and outer leaflets use the same area per molecule,
# same tail thickness, etc, but this is not necessary if the inner and outer
# leaflets are different.
inner_leaflet = LipidLeaflet(apm,
                             b_heads, v_heads, inner_head_thickness,
                             b_tails, v_tails, tail_thickness,
                             3, 3)

# we reverse the monolayer for the outer leaflet because the tail groups face upwards
outer_leaflet = LipidLeaflet(apm,
                             b_heads, v_heads, outer_head_thickness,
                             b_tails, v_tails, tail_thickness,
                             3, 0, reverse_monolayer=True)

The Slab Component represents a layer of uniform scattering length density of a given thickness in our interfacial model. Here we make Slabs from SLD objects, but other approaches are possible.

# Slab constructed from SLD object.
sio2_slab = sio2(15, 3)
sio2_slab.thick.setp(vary=True, bounds=(2, 30)) = 'sio2 thickness'
sio2_slab.rough.setp(vary=True, bounds=(0, 7)) = name='sio2 roughness'
sio2_slab.vfsolv.setp(0.1, vary=True, bounds=(0., 0.5)) = 'sio2 solvation'

solv_roughness = Parameter(3, 'bilayer/solvent roughness')
solv_roughness.setp(vary=True, bounds=(0, 5))

Once all the Components have been constructed we can chain them together to compose a Structure object. The Structure object represents the interfacial structure of our system. We create different Structures for each contrast. It is important to note that each of the Structures share many components, such as the LipidLeaflet objects. This means that parameters used to construct those components are shared between all the Structures, which enables co-refinement of multiple datasets. An alternate way to carry this out would be to apply constraints to underlying parameters, but this way is clearer. Note that the final component for each structure is a Slab created from the solvent SLDs, we give those slabs a zero thickness.

s_d2o = si | sio2_slab | inner_leaflet | outer_leaflet | d2o(0, solv_roughness)
s_hdmix = si | sio2_slab | inner_leaflet | outer_leaflet | hdmix(0, solv_roughness)
s_h2o = si | sio2_slab | inner_leaflet | outer_leaflet | h2o(0, solv_roughness)

The Structures created in the previous step describe the interfacial structure, these structures are used to create ReflectModel objects that know how to apply resolution smearing, scaling factors and background.

model_d2o = ReflectModel(s_d2o)
model_hdmix = ReflectModel(s_hdmix)
model_h2o = ReflectModel(s_h2o)

model_d2o.scale.setp(vary=True, bounds=(0.9, 1.1))

model_d2o.bkg.setp(vary=True, bounds=(-1e-6, 1e-6))
model_hdmix.bkg.setp(vary=True, bounds=(-1e-6, 1e-6))
model_h2o.bkg.setp(vary=True, bounds=(-1e-6, 1e-6))

An Objective is constructed from a ReflectDataset and ReflectModel. Amongst other things Objectives can calculate chi-squared, log-likelihood probability, log-prior probability, etc. We then combine all the individual Objectives into a GlobalObjective.

objective_d2o = Objective(model_d2o, data_d2o)
objective_hdmix = Objective(model_hdmix, data_hdmix)
objective_h2o = Objective(model_h2o, data_h2o)

global_objective = GlobalObjective([objective_d2o, objective_hdmix, objective_h2o])

A CurveFitter object can perform least squares fitting, or MCMC sampling on the Objective used to construct it.

fitter = CurveFitter(global_objective)

We’ll just do a normal least squares fit here. MCMC sampling is left as an exercise for the reader.