Commit 0db42c1c authored by Zolnierczuk, Piotr's avatar Zolnierczuk, Piotr
Browse files

Rouse model updates

parent cb719f8c
Loading
Loading
Loading
Loading
+172 −58
Original line number Diff line number Diff line
"""

==================================
NSE scattering evaluator for a Rouse polymer chain
==================================
====================================================

-----------------------------------------------------------------------
Original autho:
M. Monkenbusch
Juelich Center of Neutron Science
Forschungszentrum Juelich
Original author:
M. Monkenbusch, Juelich Center of Neutron Science Forschungszentrum Juelich
m.monkenbusch@fz-juelich.de

Fortran to Python translation:
Piotr Zolnierczuk
Piotr Zolnierczuk, Oak Ridge National Laboratory, 
zolnierczukp@ornl.gov

-----------------------------------------------------------------------
-----------------------------------------------------------

This is a little toolbox to estimate the effect of contrast and
H/D content on the dynamics data of a polymer melt in the Rouse
regime as obtained by NSE spectroscopy.
@@ -26,13 +22,11 @@ given e.g. in the book of Doi-Edwards.

But it will give a first impression on scattering contributions etc.

In particular:
consideration concerning the roles of coherent an incoherent scattering
in polymer samples
In particular: consideration concerning the roles of coherent 
and incoherent scattering in polymer samples

please observe:
-----------------------------------------------------------
units for
units
Q                             --> 1/Angstroem
lambda                        --> Angstroem

@@ -57,16 +51,20 @@ t --> nanoseconds
"""

import numpy as np
from numpy import pi, exp, cos, expm1, sqrt, mgrid, where
#pylint: disable=no-name-in-module

from numpy import pi, cos, exp, sqrt, mgrid, atleast_1d, asarray, sum as npsum, where, expm1
from scipy.special   import erfc
from scipy.integrate import quad
from scipy.constants import Boltzmann as k_B
from scipy.constants import (Boltzmann as k_B, angstrom as ANGSTROM)

#we use a bunch of physics notations
#pylint: disable=invalid-name

_debye_cut  = 1e-5  # cut-off value for f_debye
_rouse_cut  = 1e-33 # cut-off value for skernel_rouse
_rouse_eps  = 1e-6  # relative eps for Rouse kernel integration


def f_debye(x):
    """Debye function f(y)
    where y=x^2=(kR)^2"""
@@ -75,32 +73,23 @@ def f_debye(x):
    return 2/y**2*(expm1(-y)+y)  #pylint: disable=invalid-unary-operand-type


def rouse_parameters(Wl4, Re, N, T=300.0):
    """
    Calculate Rouse parameters using Wl4, Re and N
    """
    l    = Re/sqrt(N)          # segment length
    kT   = k_B * T * 100.0     # kg*m^2/s^2 -> kg * A^2/ns^2
    zeta = 3*kT*l**2/Wl4       # friction factor
    W    = 3*kT/(zeta*l**2)    # Rouse rate in 1/ns
    Dr   = kT/(N*zeta)         # diffusion constant
    tR   = (N/pi)**2/W         # Rouse time
    Rg   = Re/sqrt(6)          # radius of gyration
    return dict(W=W, l=l, zeta_0=zeta, Rg=Rg, D=Dr, tau=tR,
                Wl4=Wl4, Re=Re, N=N, T=T)

def rouse_sqt(t, q, W, Re, Dr=0, N=100, **kwargs):

def _rouse_sqt(t, q, W, Re, **kwargs):
    """
    Rouse expression for a chain of finite length:
    args::
        t    - time in ns
        q    - momentum transfer in A^(-1)
        W    - Rouse rate in 1/ns
        Re   - end-to-end distance of the polymer molecule
        Re   - end-to-end distance of the polymer molecule [A]

    kwargs::
        Dr   - center of mass diffusion constant in A**2/ns
        N    - number of chain segments
        pmin - minimum p (default 1)
        pmax - maximum p (default N)
        ninf - if True, use different efac (see below)

    returns:
        Sq   - S(Q)
        Sqt  - S(Q,t)
@@ -108,29 +97,55 @@ def rouse_sqt(t, q, W, Re, Dr=0, N=100, **kwargs):
    Based on a Fortran subroutine nrousep by M. Monkenbusch (JCNS)
    Fortran -> Python translation by P. Zolnierczuk (JCNS)
    """
    #we use a bunch of physics notations
    #pylint: disable=too-many-locals
    Dr    = kwargs.pop('Dr',   0)
    N     = kwargs.pop('N',  100)
    pmin  = kwargs.pop('pmin', None)
    pmax  = kwargs.pop('pmax', None)
    ninf  = kwargs.pop('ninf', False)

    if N <= 0:
        raise RuntimeError('Number of chain segments must be positive')
    pmin = max(kwargs.get('pmin', 1), 1)
    pmax = min(kwargs.get('pmax', N), N) + 1

    pmin = max(1,pmin or 1)
    pmax = min(N,pmax or N) + 1

    l   = Re/sqrt(N)       # calculate segment length
    fac = 2/3*(Re*q/pi)**2 #

    # ---- Do the sums -----
    PP, NN, MM  = mgrid[pmin:pmax, 0:N, 0:N]

    # exponent for S(Q) and S(Q,t)
    anm  = exp(-(q**2*abs(NN[0]-MM[0])/6*l**2))

    # exponent for S(Q,t)
    cosf = exp(-fac*np.sum(cos(pi*(NN+1)*PP/N)*cos(pi*(MM+1)*PP/N)/PP**2
                           *(1-exp(-2*W*(1-cos(pi*PP/N))*t)), axis=(0,)))
    cospp = cos(pi*(NN+1)*PP/N)*cos(pi*(MM+1)*PP/N)/PP**2
    if ninf:
        # this is for "infinite" N
        efac = (PP*pi/N)**2      #tR = (N/pi)**2/W
    else:
        # this is for "finite"   N
        efac = 2*(1-cos(pi*PP/N))

    cosf = exp(-fac*npsum(cospp*(1-exp(-efac*W*t)), axis=(0,)))

    Sq  = np.sum(anm)
    Sqt = exp(-q**2*Dr*t) * np.sum(anm*cosf)
    Sq  = npsum(anm)
    Sqt = exp(-q**2*Dr*t) * npsum(anm*cosf)

    return Sq/N, Sqt/N


def rouse_sqt(t, q, W, Re, **kwargs):
    "vectorize in tau"
    res = [ _rouse_sqt(_t, q, W, Re, **kwargs) for _t in atleast_1d(t) ]
    return asarray(res)
rouse_sqt.__doc__ = _rouse_sqt.__doc__

# ###################################################################################
# WARNING THIS IS OBSOLETE APPROXIMATION FOR QR>>1
# THIS IS APPROXIMATION FOR QR>>1
# one should use rouse_sqt
# Rouse kernel $h(u) = \frac{2}{\pi} \int_0^\infty \cos(ux)/x^2 (1-e^{-x^2}) dx$
#              $h(u) = \frac{2}{\sqrt{pi}} \exp{-(u/2)^2} - u erfc(u/2) $
def h_rouse_hiq(u):
@@ -147,10 +162,109 @@ def skernel_rouse_hiq(u, xot):
    return exp(-(u+xot*h_rouse_hiq(u/xot)))

#  $ S(Q,t)/S(Q,0) = \int_0^\infty dx exp(-u + \sqrt(omegat)
def sqt_rouse_hiq(xomegat):
def rouse_sqt_hiq(omegat):
    "Rouse dynamic structure factor"
    if np.isscalar(xomegat):
        return quad(skernel_rouse_hiq, 0, np.inf, epsrel=_rouse_eps,args=(xomegat,))
    res = [    quad(skernel_rouse_hiq, 0, np.inf, epsrel=_rouse_eps,args=(_xot,   ))
               for _xot in xomegat ]
    return np.asarray(res)
    res = [ quad(skernel_rouse_hiq, 0, np.inf, epsrel=_rouse_eps, args=(_x,))
            for _x in atleast_1d(omegat) ]
    return asarray(res)


class RouseModel:
    """Rouse parameters
    units:
        - time ns
        - length: A  (RE, Rg)
        - temperature: K
        - zeta: N ns/A
    """
    def __init__(self, W, l, N, T=300):
        self.W    = W # elemetary Rouse rate [1/ns]
        self.l    = l # segment length [A]
        self.N    = N # number of segments
        self.kT   = k_B*T # kT

    def __str__(self):
        return ", ".join([f"{k}={v:g}" for k,v in self.to_dict().items()])

    def units(self):
        "units"
        return "units=A,ns,K, zeta=N*ns/A"

    def to_dict(self):
        "convert to dictionary"
        return  {'W'   :self.W ,  'l' :self.l,
                 'N'   :self.N,   'kT':self.kT,
                 'RE'  :self.RE,  'Rg':self.Rg,
                 'DR'  :self.DR,  'tR':self.tR, 
                 'Wl4' :self.Wl4, 'zeta': self.zeta}

    @classmethod
    def from_wl4(cls, Wl4, RE, N, T):
        "Calculate Rouse parameters using Wl4, Re and N"
        l    = RE/sqrt(N)  # segment length
        return RouseModel(Wl4/l**4,l,N,T)

    @property
    def RE(self):
        "end-to-end radius in A"
        return sqrt(self.N*self.l**2)
    @property
    def Rg(self):
        "Radius of gyration in A"
        return self.RE/sqrt(6)
    @property
    def zeta(self):
        "zeta in N*ns/A"
        return 3*self.kT/(self.W*self.l**2)/ANGSTROM
    @property
    def DR(self):
        "Rouse Diffusion coefficient [A^2/ns]"
        return self.Wl4/(3*self.RE**2)
    @property
    def tR(self):
        "Rouse time [ns]"
        return (self.N/pi)**2/self.W

    @property
    def Wl4(self):
        "Wl^4 nm^4/ns"
        return self.W*self.l**4

    def omegaR(self, q):
        "Rouse <variable>"
        return (q*self.l)**4*self.W/36

    def sqt(self, t, q, **kwargs):
        """
        Rouse expression for a chain of finite length:
        args::
            t    - time in ns
            q    - momentum transfer in A^(-1)

        kwargs::
            Dr   - center of mass diffusion constant in A**2/ns
            pmin - minimum p (default 1)
            pmax - maximum p (default N)
            ninf - if True, use different efac (see below)

        returns:
            Sq   - S(Q)
            Sqt  - S(Q,t)
        """
        return rouse_sqt(t, q, W=self.W, Re=self.RE, N=self.N, **kwargs)

    def sqt_hiq(self, t, q):
        """
        Rouse expression for high Q.
        args::
            t    - time in ns
            q    - momentum transfer in A^(-1)

        returns:
            Sqt  - S(Q,t)
        """
        u = sqrt(self.omegaR(q)*atleast_1d(t))
        res = np.asarray(rouse_sqt_hiq(u))
        return res

#
+2 −2
Original line number Diff line number Diff line
@@ -3,8 +3,8 @@ PySEN revision module
"""
import sys
__version__  = "2.0"
__release__  = "a8"
__date__     = "Nov 14, 2024"
__release__  = "a9"
__date__     = "Dec 19, 2024"

def version(full=False):
    "get pysen version number"
+104 −47
Original line number Diff line number Diff line
@@ -6,76 +6,78 @@ import unittest
import numpy as np
import scipy.integrate as integ

import pysen.physics.nscatt as nscatt
from numpy import sqrt, log10, pi
from scipy.constants import Boltzmann as kB

from pysen.physics.nscatt import (RouseModel, f_debye,
                                  h_rouse_hiq, rouse_sqt_hiq, rouse_sqt)

class NeutronScatteringTestCase(unittest.TestCase):
    "Neutron Scattering Test Cases"

    def setUp(self):
        self.Wl4 = 39500.0
        self.Re  =    40.0
        self.T   =   300.0
        self.N   =   100
        pass
        # values from Richter, Monkenbusch, Arbe, Colmenero (2005)
        # for PEP at 423K
        self.T =   423               # temperature [K]
        self.N =   100               # number of segments
        self.Wl4 = 0.95e4            # Rouse parameter Wl^4 [A^4/ns]
        self.RE  = sqrt(self.N*56.5) # end-to-end radius [A]

    def tearDown(self):
        pass

    def test_f_debye(self):
        "test for Debye function"
        inp = np.array([
        #     x    f(x)
            ( 0.0, 1.0000),
            ( 1.0, 0.7358),
            ( 2.0, 0.3773),
            (50.0, 0.0008), ])
        fx = nscatt.f_debye(inp[:,0])
        fx = f_debye(inp[:,0])
        self.assertTrue(np.allclose(fx, inp[:,1], atol=1e-03))


        #
        I0 = integ.quad(nscatt.f_debye, 0, np.infty)
        self.assertAlmostEqual(4/3*np.sqrt(np.pi), I0[0], 5, "f_debye integral (value)")
        I0 = integ.quad(f_debye, 0, np.infty)
        self.assertAlmostEqual(4/3*sqrt(pi), I0[0], 5, "f_debye integral (value)")
        self.assertAlmostEqual(0           , I0[1], 5, "f_debye integral (error)")


    def test_rouse_hkernel_hiq(self):
    def test_rouse_parameters(self):
        "test for Rouse model parameters"
        rpar = RouseModel.from_wl4(self.Wl4, self.RE, self.N, self.T)
        #print("\n",rpar)
        N  = self.N
        l2 = self.RE**2/N # l^2
        logW  = log10(self.Wl4/(l2**2))             # W = Wl4/(l^2)^2
        logDR = log10(self.Wl4/(3*self.RE**2))      # DR = Wl4/(3*RE^2)
        logtR = log10((self.RE**2/pi)**2/self.Wl4)  # tR = (RE/pi)^2/Wl4
        logZeta = log10(3*kB*self.T/self.Wl4*l2)+10 # +10 from m to A
        #
        self.assertAlmostEqual(rpar.kT/kB,       self.T          , 5, "RouseParameter T")
        self.assertAlmostEqual(rpar.N,           self.N          , 5, "RouseParameter N")
        self.assertAlmostEqual(rpar.Wl4,         self.Wl4        , 5, "RouseParameter Wl4")
        self.assertAlmostEqual(rpar.RE,          self.RE         , 5, "RouseParameter RE")
        self.assertAlmostEqual(rpar.l,           self.RE/sqrt(N) , 5, "RouseParameter ell")
        self.assertAlmostEqual(rpar.Rg,          self.RE/sqrt(6) , 5, "RouseParameter Rg")
        self.assertAlmostEqual(log10(rpar.W),    logW            , 5, "RouseParameter W ")
        self.assertAlmostEqual(log10(rpar.DR),   logDR           , 5, "RouseParameter DR")
        self.assertAlmostEqual(log10(rpar.tR),   logtR           , 5, "RouseParameter tR")
        self.assertAlmostEqual(log10(rpar.zeta), logZeta         , 5, "RouseParameter zeta")

    def test_rouse_kernel_hiq(self):
        "test for Rouse hiQ kernel"
        inp = np.array([
           #    u    h(u)
           ( 0.0, 2/np.sqrt(np.pi)),
           ( 1.0, 0.3993),
           ( 2.0, 0.1005),
           (10.0, 0.0000), ])
        hu = nscatt.h_rouse_hiq(inp[:,0])
        hu = h_rouse_hiq(inp[:,0])
        self.assertTrue(np.allclose(hu, inp[:,1], atol=1e-03))

    def test_sqt_rouse_hiq(self):
        inp = np.array([
        #    omegat  S(omegat)
            ( 0.0,   1.0000),
            ( 1.0,   0.6088),
            ( 2.0,   0.2620),
            ( 5.0,   0.0135),
            (10.0,   0.0000), ])
        sqt = nscatt.sqt_rouse_hiq(inp[:,0])[:,0]
        self.assertTrue(np.allclose(sqt, inp[:,1], atol=1e-03), sqt)

    def test_rouse_parameters(self):

        r = nscatt.rouse_parameters(Wl4=self.Wl4, Re=self.Re, T=self.T, N=self.N)
        # input
        self.assertEqual(self.N , r['N'] , "Number of segments")
        self.assertAlmostEqual(self.Wl4    , r['Wl4']        , 5, "Wl4: Rouse rate")
        self.assertAlmostEqual(self.Re     , r['Re']         , 5, "end to end length")
        self.assertAlmostEqual(self.T      , r['T']          , 5, "temperature")
        # results
        self.assertAlmostEqual(  4.0       , r['l']          , 5, "l: segment length")
        self.assertAlmostEqual( 16.3299316 , r['Rg']         , 5, "radius of gyration")
        self.assertAlmostEqual(  5.033251  , r['zeta_0']*1e22, 5, "zeta: friction constant")
        self.assertAlmostEqual(154.296875  , r['W']          , 5, "W: Rouse rate")
        self.assertAlmostEqual(  6.566638  , r['tau']        , 5, "tR: Rouse time")
        self.assertAlmostEqual(  8.229167  , r['D']          , 5, "D: Rouse diffusion rate")

    def test_rouse_sqt(self):
    def test_rouse_sqt1(self):
        "test for Rouse Sqt (vs datreat)"
        # results from datreat
        #                  tau      S(Q)      S(Q,t)
        d = np.asarray( [[ 0.00000, 48.835216, 48.835216],
@@ -84,12 +86,67 @@ class NeutronScatteringTestCase(unittest.TestCase):
                         [30.00000, 48.835216, 42.692559],
                         [40.00000, 48.835216, 42.692053],
                         [50.00000, 48.835216, 42.692025]])
        W = nscatt.rouse_parameters(Wl4=self.Wl4, Re=self.Re, T=self.T, N=self.N)['W']
        q   = 0.1
        rmod = RouseModel.from_wl4(Wl4=39500.0, RE=40.0, N=100, T=300)
        # first loop direct rouse_sqt
        for tau, Sq, Sqt in d:
            act = rouse_sqt(tau, q , W=rmod.W, Re=rmod.RE, N=rmod.N, pmin=1, pmax=rmod.N)
            self.assertAlmostEqual(Sq,  act[0,0], 5, "S(Q)")
            self.assertAlmostEqual(Sqt, act[0,1], 5, "S(Q,t)")
        # second loop direct model method
        for tau, Sq, Sqt in d:
            act_Sq, act_Sqt = nscatt.rouse_sqt(tau, q , W, self.Re, N=self.N, pmin=1, pmax=self.N)
            self.assertAlmostEqual(Sq,  act_Sq,  4, "S(Q)")
            self.assertAlmostEqual(Sqt, act_Sqt, 5, "S(Q,t)")
            act = rmod.sqt(tau, q , pmin=1, pmax=rmod.N)
            self.assertAlmostEqual(Sq,  act[0,0], 5, "S(Q)")
            self.assertAlmostEqual(Sqt, act[0,1], 5, "S(Q,t)")


    def test_rouse_sqt2(self):
        "test for Rouse Sqt"
        # results for PEP@423
        #   S(Q)       S(Q,t)
        data = [[5.00501524, 5.00501524],
                [5.00501524, 2.16188872],
                [5.00501524, 1.34832783],
                [5.00501524, 0.93827628],
                [5.00501524, 0.69473812],
                [5.00501524, 0.53632341]]
        expected = np.asarray(data)

        rmod = RouseModel.from_wl4(self.Wl4, self.RE, self.N, self.T)
        actual = rmod.sqt([0,5,10,15,20,25], 2*pi/rmod.Rg  , pmax=rmod.N)
        self.assertTrue(np.allclose(actual, expected, atol=1e-10), "rouse_sqt2")

    def test_sqt_rouse_hiq1(self):
        "test for Rouse Sqt (high Q)"
        inp = np.array([
        #    omegat  S(omegat)
            ( 0.0,   1.0000),
            ( 1.0,   0.6088),
            ( 2.0,   0.2620),
            ( 5.0,   0.0135),
            (10.0,   0.0000), ])
        sqt = rouse_sqt_hiq(inp[:,0])[:,0]
        self.assertTrue(np.allclose(sqt, inp[:,1], atol=1e-03), "sqt_hiq1")

    def test_sqt_rouse_hiq2(self):
        "test for Rouse Sqt (high Q)"
        inp = np.array([
        #    omegat  S(omegat)
            ( 0.0,   1.0000),
            ( 1.0,   0.6088),
            ( 2.0,   0.2620),
            ( 5.0,   0.0135),
            (10.0,   0.0000), ])

        u, expected = inp[:,0], inp[:,1]

        rmod = RouseModel.from_wl4(self.Wl4, self.RE, self.N, self.T)
        q=2*pi/rmod.Rg

        taus = u**2/rmod.omegaR(q)
        actual   = rmod.sqt_hiq(taus, q)[:,0]
        self.assertTrue(np.allclose(actual, expected, atol=1e-4), "rouse_hiq2")


if __name__ == "__main__":
    unittest.main(exit=False, verbosity=2)