Source code for refnx.reflect._lipid

"""
Component for studying lipid membranes at an interface
"""

import warnings
import numpy as np
from scipy.optimize import NonlinearConstraint
from refnx.reflect import Component, SLD, ReflectModel, Structure
from refnx.analysis import possibly_create_parameter, Parameters, Parameter
from refnx.reflect.structure import (
    overall_sld,
    Scatterer,
    possibly_create_scatterer,
)


[docs] class LipidLeaflet(Component): r""" Describes a lipid leaflet Component at an interface Parameters ---------- APM: float or refnx.analysis.Parameter b_heads: float, refnx.analysis.Parameter, complex or SLD Sum of coherent scattering lengths of head group (Angstrom). When an SLD is provided it is simply an easy way to provide a complex value. `LipidLeaflet.b_heads_real` is set to `SLD.real`, etc. vm_heads: float or refnx.analysis.Parameter Molecular volume of head group (Angstrom**2) thickness_heads: float or refnx.analysis.Parameter Thickness of head group region (Angstrom) b_tails: float, refnx.analysis.Parameter, complex or SLD Sum of coherent scattering lengths of tail group (Angstrom). When an SLD is provided it is simply an easy way to provide a complex value. `LipidLeaflet.b_tails_real` is set to `SLD.real`, etc. vm_tails: float or refnx.analysis.Parameter Molecular volume of tail group (Angstrom**2) thickness_tails: float or refnx.analysis.Parameter Thickness of head group region (Angstrom) rough_head_tail: float or refnx.analysis.Parameter Roughness of head-tail group (Angstrom) rough_preceding_mono: float or refnx.analysis.Parameter Roughness between preceding component (in the fronting direction) and the monolayer (Angstrom). If `reverse_monolayer is False` then this is the roughness between the preceding component and the heads, if `reverse_monolayer is True` then this is the roughness between the preceding component and the tails. head_solvent: None, float, complex, refnx.reflect.SLD Solvent for the head region. If `None`, then solvation will be performed by the parent `Structure`, using the `Structure.solvent` attribute. Other options are coerced to an `SLD` object using `SLD(float | complex)`. A float/complex argument is the SLD of the solvent (10**-6 Angstrom**-2). tail_solvent: None, float, complex, refnx.reflect.SLD Solvent for the tail region. If `None`, then solvation will be performed by the parent `Structure`, using the `Structure.solvent` attribute. Other options are coerced to an `SLD` object using `SLD(float | complex)`. A float/complex argument is the SLD of the solvent (10**-6 Angstrom**-2). reverse_monolayer: bool, optional The default is to have heads closer to the fronting medium and tails closer to the backing medium. If `reverse_monolayer is True` then the tails will be closer to the fronting medium and heads closer to the backing medium. name: str, optional The name for the component Notes ----- The sum of coherent scattering lengths must be in Angstroms, the volume must be in cubic Angstroms. This is because the SLD of a tail group is calculated as `b_tails / vm_tails * 1e6` to achieve the units 10**6 Angstrom**-2. """ # TODO: use SLD of head instead of b_heads, vm_heads? def __init__( self, apm, b_heads, vm_heads, thickness_heads, b_tails, vm_tails, thickness_tails, rough_head_tail, rough_preceding_mono, head_solvent=None, tail_solvent=None, reverse_monolayer=False, name="", ): """ Parameters ---------- apm: float or Parameter Area per molecule b_heads: float, Parameter or complex Sum of coherent scattering lengths of head group (Angstrom) vm_heads: float or Parameter Molecular volume of head group (Angstrom**3) thickness_heads: float or Parameter Thickness of head group region (Angstrom) b_tails: float, Parameter or complex Sum of coherent scattering lengths of tail group (Angstrom) vm_tails: float or Parameter Molecular volume of tail group (Angstrom**3) thickness_tails: float or Parameter Thickness of head group region (Angstrom) rough_head_tail: float or refnx.analysis.Parameter Roughness of head-tail group (Angstrom) rough_preceding_mono: float or Parameter Roughness between preceding component (in the fronting direction) and the monolayer (Angstrom). If `reverse_monolayer is False` then this is the roughness between the preceding component and the heads, if `reverse_monolayer is True` then this is the roughness between the preceding component and the tails. head_solvent: None, float, complex, SLD Solvent for the head region. If `None`, then solvation will be performed by the parent `Structure`, using the `Structure.solvent` attribute. Other options are coerced to an `SLD` object using `SLD(float | complex)`. A float/complex argument is the SLD of the solvent (10**-6 Angstrom**-2). tail_solvent: None, float, complex, SLD Solvent for the tail region. If `None`, then solvation will be performed by the parent `Structure`, using the `Structure.solvent` attribute. Other options are coerced to an `SLD` object using `SLD(float | complex)`. A float/complex argument is the SLD of the solvent (10**-6 Angstrom**-2). reverse_monolayer: bool, optional The default is to have heads closer to the fronting medium and tails closer to the backing medium. If `reverse_monolayer is True` then the tails will be closer to the fronting medium and heads closer to the backing medium. name: str, optional The name for the component """ super().__init__() self.apm = possibly_create_parameter( apm, "%s - area_per_molecule" % name, units="Å**2" ) if isinstance(b_heads, complex): self.b_heads_real = possibly_create_parameter( b_heads.real, name="%s - b_heads_real" % name ) self.b_heads_imag = possibly_create_parameter( b_heads.imag, name="%s - b_heads_imag" % name ) elif isinstance(b_heads, SLD): self.b_heads_real = b_heads.real self.b_heads_imag = b_heads.imag else: self.b_heads_real = possibly_create_parameter( b_heads, name="%s - b_heads_real" % name ) self.b_heads_imag = possibly_create_parameter( 0, name="%s - b_heads_imag" % name ) self.b_heads_real.units = self.b_heads_imag.units = "Å" self.vm_heads = possibly_create_parameter( vm_heads, name="%s - vm_heads" % name, units="Å**3" ) self.thickness_heads = possibly_create_parameter( thickness_heads, name="%s - thickness_heads" % name, units="Å" ) if isinstance(b_tails, complex): self.b_tails_real = possibly_create_parameter( b_tails.real, name="%s - b_tails_real" % name ) self.b_tails_imag = possibly_create_parameter( b_tails.imag, name="%s - b_tails_imag" % name ) elif isinstance(b_tails, SLD): self.b_tails_real = b_tails.real self.b_tails_imag = b_tails.imag else: self.b_tails_real = possibly_create_parameter( b_tails, name="%s - b_tails_real" % name ) self.b_tails_imag = possibly_create_parameter( 0, name="%s - b_tails_imag" % name ) self.b_tails_real.units = self.b_tails_imag.units = "Å" self.vm_tails = possibly_create_parameter( vm_tails, name="%s - vm_tails" % name, units="Å**3" ) self.thickness_tails = possibly_create_parameter( thickness_tails, name="%s - thickness_tails" % name, units="Å" ) self.rough_head_tail = possibly_create_parameter( rough_head_tail, name="%s - rough_head_tail" % name, units="Å" ) self.rough_preceding_mono = possibly_create_parameter( rough_preceding_mono, name="%s - rough_fronting_mono" % name, units="Å", ) self.head_solvent = self.tail_solvent = None if head_solvent is not None: self.head_solvent = possibly_create_scatterer(head_solvent) if tail_solvent is not None: self.tail_solvent = possibly_create_scatterer(tail_solvent) self.reverse_monolayer = reverse_monolayer self.name = name def __repr__(self): sld_bh = SLD([self.b_heads_real, self.b_heads_imag]) sld_bt = SLD([self.b_tails_real, self.b_tails_imag]) s = ( f"LipidLeaflet(" f"{self.apm!r}, " f"{sld_bh!r}, " f"{self.vm_heads!r}, " f"{self.thickness_heads!r}, " f"{sld_bt!r}, " f"{self.vm_tails!r}, " f"{self.thickness_tails!r}, " f"{self.rough_head_tail!r}, " f"{self.rough_preceding_mono!r}, " f"head_solvent={self.head_solvent!r}, " f"tail_solvent={self.tail_solvent!r}, " f"reverse_monolayer={self.reverse_monolayer}, " f"name={self.name!r})" ) return s
[docs] def slabs(self, structure=None): """ Slab representation of monolayer, as an array Parameters ---------- structure : refnx.reflect.Structure The Structure hosting this Component """ layers = np.zeros((2, 5)) # thicknesses layers[0, 0] = float(self.thickness_heads) layers[1, 0] = float(self.thickness_tails) # real and imag SLD's layers[0, 1] = float(self.b_heads_real) / float(self.vm_heads) * 1.0e6 layers[0, 2] = float(self.b_heads_imag) / float(self.vm_heads) * 1.0e6 layers[1, 1] = float(self.b_tails_real) / float(self.vm_tails) * 1.0e6 layers[1, 2] = float(self.b_tails_imag) / float(self.vm_tails) * 1.0e6 # roughnesses layers[0, 3] = float(self.rough_preceding_mono) layers[1, 3] = float(self.rough_head_tail) # volume fractions # head region layers[0, 4] = 1 - self.volfrac_h if self.head_solvent is not None: _head_solvent = self.head_solvent.complex( getattr(structure, "wavelength", None) ) # we do the solvation here, not in Structure.slabs layers[0] = overall_sld(layers[0], _head_solvent) layers[0, 4] = 0 # tail region layers[1, 4] = 1 - self.volfrac_t if self.tail_solvent is not None: _tail_solvent = self.tail_solvent.complex( getattr(structure, "wavelength", None) ) # we do the solvation here, not in Structure.slabs layers[1] = overall_sld(layers[1], _tail_solvent) layers[1, 4] = 0 if self.reverse_monolayer: layers = np.flipud(layers) layers[:, 3] = layers[::-1, 3] return layers
@property def parameters(self): p = Parameters(name=self.name) p.extend( [ self.apm, self.b_heads_real, self.b_heads_imag, self.vm_heads, self.thickness_heads, self.b_tails_real, self.b_tails_imag, self.vm_tails, self.thickness_tails, self.rough_head_tail, self.rough_preceding_mono, ] ) if self.head_solvent is not None: p.append(self.head_solvent.parameters) if self.tail_solvent is not None: p.append(self.tail_solvent.parameters) return p
[docs] def logp(self): # penalise unphysical volume fractions. if self.volfrac_h > 1 or self.volfrac_t > 1: return -np.inf return 0
@property def volfrac_h(self): # Volume fraction of head group in head group region return self.vm_heads.value / ( self.apm.value * self.thickness_heads.value ) @property def volfrac_t(self): # Volume fraction of tail group in tail group region return self.vm_tails.value / ( self.apm.value * self.thickness_tails.value )
[docs] def make_constraint(self, objective): """ Creates a NonlinearConstraint for a LipidLeaflet, ensuring that volume fraction of lipid in the head+tail regions lies in [0, 1]. Suitable for use by differential_evolution. Parameters ---------- objective: refnx.analysis.Objective Objective containing the LipidLeaflet. Must be the Objective that is being minimised by differential_evolution. Returns ------- nlc: NonlinearConstraint Notes ----- You must create separate constraints for each LipidLeaflet object in your system. The Objective you supply must be for the overall curve fitting system. i.e. possibly a GlobalObjective. Examples -------- >>> # leaflet is a LipidLeaflet, used in an Objective, obj >>> con = leaflet.make_constraint(obj) >>> fitter = CurveFitter(obj) >>> fitter.fit("differential_evolution", constraints=(con,)) """ def con(x): objective.setp(x) return self.volfrac_h, self.volfrac_t return NonlinearConstraint(con, 0, 1)
[docs] class LipidLeafletGuest(LipidLeaflet): r""" Describes a lipid leaflet Component at an interface with an incorporated guest molecule. Parameters ---------- APM: float or refnx.analysis.Parameter b_heads: float, refnx.analysis.Parameter, complex or SLD Sum of coherent scattering lengths of head group (Angstrom). When an SLD is provided it is simply an easy way to provide a complex value. `LipidLeaflet.b_heads_real` is set to `SLD.real`, etc. vm_heads: float or refnx.analysis.Parameter Molecular volume of head group (Angstrom**2) thickness_heads: float or refnx.analysis.Parameter Thickness of head group region (Angstrom) b_tails: float, refnx.analysis.Parameter, complex or SLD Sum of coherent scattering lengths of tail group (Angstrom). When an SLD is provided it is simply an easy way to provide a complex value. `LipidLeaflet.b_tails_real` is set to `SLD.real`, etc. vm_tails: float or refnx.analysis.Parameter Molecular volume of tail group (Angstrom**2) thickness_tails: float or refnx.analysis.Parameter Thickness of head group region (Angstrom) rough_head_tail: float or refnx.analysis.Parameter Roughness of head-tail group (Angstrom) rough_preceding_mono: float or refnx.analysis.Parameter Roughness between preceding component (in the fronting direction) and the monolayer (Angstrom). If `reverse_monolayer is False` then this is the roughness between the preceding component and the heads, if `reverse_monolayer is True` then this is the roughness between the preceding component and the tails. phi_guest_h: float or refnx.analysis.Parameter Guest lying in the head layer. This is a fractional value representing how much of the space **not** taken up by the lipid is occupied by the guest molecule. The absolute volume fraction is available from the `LipidLeafletGuest.volfrac_guest` property. .. warning:: This parameter may not be determinable with low uncertainty if the lipid occupies nearly all of the space in the layer. For best results the guest and tail region should have very different SLDs. phi_guest_t: float or refnx.analysis.Parameter Guest lying in the tail layer. This is a fractional value representing how much of the space **not** taken up by the lipid is occupied by the guest molecule. The absolute volume fraction is available from the `LipidLeafletGuest.volfrac_guest` property. .. warning:: This parameter may not be determinable with low uncertainty if the lipid occupies nearly all of the space in the layer. For best results the guest and tail region should have very different SLDs. sld_guest: None, float, complex, refnx.reflect.SLD SLD of guest molecule. head_solvent: None, float, complex, refnx.reflect.SLD Solvent for the head region. If `None`, then solvation will be performed by the parent `Structure`, using the `Structure.solvent` attribute. Other options are coerced to an `SLD` object using `SLD(float | complex)`. A float/complex argument is the SLD of the solvent (10**-6 Angstrom**-2). tail_solvent: None, float, complex, refnx.reflect.SLD Solvent for the tail region. If `None`, then solvation will be performed by the parent `Structure`, using the `Structure.solvent` attribute. Other options are coerced to an `SLD` object using `SLD(float | complex)`. A float/complex argument is the SLD of the solvent (10**-6 Angstrom**-2). reverse_monolayer: bool, optional The default is to have heads closer to the fronting medium and tails closer to the backing medium. If `reverse_monolayer is True` then the tails will be closer to the fronting medium and heads closer to the backing medium. name: str, optional The name for the component Notes ----- The sum of coherent scattering lengths must be in Angstroms, the volume must be in cubic Angstroms. This is because the SLD of a tail group is calculated as `b_tails / vm_tails * 1e6` to achieve the units 10**6 Angstrom**-2. """ def __init__( self, apm, b_heads, vm_heads, thickness_heads, b_tails, vm_tails, thickness_tails, rough_head_tail, rough_preceding_mono, phi_guest_h, phi_guest_t, sld_guest, head_solvent=None, tail_solvent=None, reverse_monolayer=False, name="", ): super().__init__( apm, b_heads, vm_heads, thickness_heads, b_tails, vm_tails, thickness_tails, rough_head_tail, rough_preceding_mono, head_solvent=head_solvent, tail_solvent=tail_solvent, reverse_monolayer=reverse_monolayer, name=name, ) warnings.warn( "LipidLeafletGuest changed in v0.1.60 to allow guest molecules" " in the head region as well. The number AND order of parameters" " has changed. Please double check your usage", RuntimeWarning, stacklevel=2, ) self.phi_guest_h = possibly_create_parameter(phi_guest_h) self.phi_guest_h.bounds.lb = 0 self.phi_guest_h.bounds.ub = 1 self.phi_guest_t = possibly_create_parameter(phi_guest_t) self.phi_guest_t.bounds.lb = 0 self.phi_guest_t.bounds.ub = 1 self.sld_guest = possibly_create_scatterer(sld_guest) def __repr__(self): sld_bh = SLD([self.b_heads_real, self.b_heads_imag]) sld_bt = SLD([self.b_tails_real, self.b_tails_imag]) s = ( f"LipidLeaflet(" f"{self.apm!r}, " f"{sld_bh!r}, " f"{self.vm_heads!r}, " f"{self.thickness_heads!r}, " f"{sld_bt!r}, " f"{self.vm_tails!r}, " f"{self.thickness_tails!r}, " f"{self.rough_head_tail!r}, " f"{self.rough_preceding_mono!r}, " f"{self.phi_guest_h!r}, " f"{self.phi_guest_t!r}, " f"{self.sld_guest!r}, " f"head_solvent={self.head_solvent!r}, " f"tail_solvent={self.tail_solvent!r}, " f"reverse_monolayer={self.reverse_monolayer}, " f"name={self.name!r})" ) return s
[docs] def slabs(self, structure=None): """ Slab representation of monolayer, as an array Parameters ---------- structure : refnx.reflect.Structure The Structure hosting this Component """ layers = np.zeros((2, 5)) _sld_guest = complex(self.sld_guest) # thicknesses layers[0, 0] = float(self.thickness_heads) layers[1, 0] = float(self.thickness_tails) # sld of guest and tail mixture. water is added on later vfh = self.volfrac_h vfhg = self.volfrac_guest_h re_sld_head = float(self.b_heads_real) / float(self.vm_heads) * 1.0e6 im_sld_head = float(self.b_heads_imag) / float(self.vm_heads) * 1.0e6 den = vfh + vfhg layers[0, 1] = (vfh * re_sld_head + vfhg * _sld_guest.real) / den layers[0, 2] = (vfh * im_sld_head + vfhg * _sld_guest.imag) / den # sld of guest and tail mixture. water is added on later vft = self.volfrac_t vftg = self.volfrac_guest_t re_sld_tail = float(self.b_tails_real) / float(self.vm_tails) * 1.0e6 im_sld_tail = float(self.b_tails_imag) / float(self.vm_tails) * 1.0e6 den = vft + vftg layers[1, 1] = (vft * re_sld_tail + vftg * _sld_guest.real) / den layers[1, 2] = (vft * im_sld_tail + vftg * _sld_guest.imag) / den # roughnesses layers[0, 3] = float(self.rough_preceding_mono) layers[1, 3] = float(self.rough_head_tail) # volume fractions # head region # calculate solvation amount layers[0, 4] = 1 - vfh - vfhg if self.head_solvent is not None: _head_solvent = self.head_solvent.complex( getattr(structure, "wavelength", None) ) # we do the solvation here, not in Structure.slabs layers[0] = overall_sld(layers[0], _head_solvent) layers[0, 4] = 0 # tail region # calculate solvation amount layers[1, 4] = 1 - vft - vftg if self.tail_solvent is not None: _tail_solvent = self.tail_solvent.complex( getattr(structure, "wavelength", None) ) # we do the solvation here, not in Structure.slabs layers[1] = overall_sld(layers[1], _tail_solvent) layers[1, 4] = 0 if self.reverse_monolayer: layers = np.flipud(layers) layers[:, 3] = layers[::-1, 3] return layers
@property def parameters(self): p = Parameters(name=self.name) p.extend( [ self.apm, self.b_heads_real, self.b_heads_imag, self.vm_heads, self.thickness_heads, self.b_tails_real, self.b_tails_imag, self.vm_tails, self.thickness_tails, self.rough_head_tail, self.rough_preceding_mono, self.phi_guest_h, self.phi_guest_t, ] ) p.append(self.sld_guest.parameters) if self.head_solvent is not None: p.append(self.head_solvent.parameters) if self.tail_solvent is not None: p.append(self.tail_solvent.parameters) return p
[docs] def logp(self): # penalise unphysical volume fractions. if ( self.volfrac_h > 1 or self.volfrac_t > 1 or self.phi_guest_h.value > 1 or self.phi_guest_t.value > 1 ): return -np.inf return 0
@property def volfrac_guest_h(self): # Absolute volume fraction of guest in the head group region. vfh = self.volfrac_h return (1.0 - vfh) * self.phi_guest_h.value @property def volfrac_guest_t(self): # Absolute volume fraction of guest in the tail group region. vft = self.volfrac_t return (1.0 - vft) * self.phi_guest_t.value