File: maier_saupe.py

package info (click to toggle)
sasmodels 1.0.10-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 16,488 kB
  • sloc: python: 26,277; ansic: 8,036; makefile: 150; sh: 63
file content (132 lines) | stat: -rw-r--r-- 4,591 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from __future__ import print_function
import numpy as np
from numpy import exp, sin, degrees, radians, pi, sqrt

from sasmodels.weights import Dispersion as BaseDispersion

class Dispersion(BaseDispersion):
    r"""
    Maier-Saupe dispersion on orientation.

    .. math:

        w(\theta) = e^{a{\cos^2 \theta}}

    This provides a close match to the gaussian distribution for
    low angles, but the tails are limited to $\pm 90^\circ$.  For $a \ll 1$
    the distribution is approximately uniform.  The usual polar coordinate
    projection applies, with $\theta$ weights scaled by $\cos \theta$
    and $\phi$ weights unscaled.

    This is equivalent to a cyclic gaussian distribution
    $w(\theta) = e^{-sin^2(\theta)/(2\a^2)}.

    Note that the incorrect distribution is used for $a=0$.  With the
    distribution parameter labelled as *width*, the value *width=0* is
    is assumed to be completely oriented and no polydispersity distribution
    is generated.  However a value of $a=0$ should be completely unoriented.
    Any small value (e.g., 0.01 or lower) should suffice.

    The order parameter $P_2$ is defined as

    .. math:

        P(a, \beta) = \frac{e^{a \cos^2 \beta}}{
                       4\pi \int_0^{\pi/2} e^{a \cos^2 \beta} \sin \beta\,d\beta}

        P_2 = 4\pi \int_0^{\pi/2} \frac{3 \cos^2 \beta - 1)}{2}
                        P(a, \beta) \sin \beta\,d\beta

    where $a$ is the distribution width $\sigma$ handed to the weights function.
    There is a somewhat complicated closed form solution

    .. math:

        P_2 = \frac{3e^a}{2\sqrt{\pi a} E_a} - \frac{3}{4a} - \frac{1}{2}

    where $E_a = \mathrm{erfi}(\sqrt{a})$ is the imaginary error function,
    which can be coded in python as::

        from numpy import pi, sqrt, exp
        from scipy.special import erfi

        def P_2(a):
            # protect against overflow with a Taylor series at infinity
            if a <= 700:
                r = exp(a)/sqrt(pi*a)/erfi(sqrt(a))
            else:
                r = 1/((((6.5525/a + 1.875)/a + 0.75)/a + 0.5)/a + 1)
            return 1.5*r - 0.75/a - 0.5

    Given an order parameter $S = P_2(a)$, one can also solve for the
    equivalent $a$:

        from scipy.optimize import fsolve

        def P_2_inv(S):
            return fsolve(lambda x: P_2(x) - S, 1.0)[0]

    References
    ----------

    [1] Hardouin, et al., 1995. SANS study of a semiflexible main chain
    liquid crystalline polyether. *Macromolecules* 28, 5427-5433.
    """
    type = "maier_saupe"
    default = dict(npts=35, width=1, nsigmas=None)

    # Note: center is always zero for orientation distributions
    def _weights(self, center, sigma, lb, ub):
        # use the width parameter as the value for Maier-Saupe "a"
        # and find the equivalent width sigma
        a = sigma
        sigma = 1./sqrt(2.*a)

        # Limit width to +/- 90 degrees
        width = min(self.nsigmas*sigma, pi/2)
        x = np.linspace(-width, width, self.npts)

        # Truncate the distribution in case the parameter value is limited
        x[(x >= radians(lb)) & (x <= radians(ub))]

        # Return orientation in degrees with Maier-Saupe weights
        # Note: weights are normalized to sum to 1, so we can scale
        # by an arbitrary scale factor c = exp(m) to get:
        #     w = exp(m*cos(x)**2)/c = exp(-m*sin(x)**2)
        return degrees(x), exp(-a*sin(x)**2)



def P_2(a):
    from numpy import pi, sqrt, exp
    from scipy.special import erfi

    # Double precision e^x overflows so do a Taylor expansion around infinity
    # for large values of a.  With five terms it is accurate to 12 digits
    # at a = 700, and 7 digits at a = 75.
    if a <= 700:
        r = exp(a)/sqrt(pi*a)/erfi(sqrt(a))
    else:
        r = 1/((((6.5525/a + 1.875)/a + 0.75)/a + 0.5)/a + 1)
    return 1.5*r - 0.75/a - 0.5

def P_2_inv(S):
    from scipy.optimize import fsolve
    return fsolve(lambda x: P_2(x) - S, 1.0)[0]

def P_2_numerical(a):
    from scipy.integrate import romberg
    from numpy import cos, sin, pi, exp
    def num(beta):
        return (1.5 * cos(beta)**2 - 0.5) * exp(a * cos(beta)**2) * sin(beta)
    def denom(beta):
        return exp(a * cos(beta)**2) * sin(beta)
    return romberg(num, 0, pi/2) / romberg(denom, 0, pi/2)

if __name__ == "__main__":
    import sys
    a = float(sys.argv[1])
    sigma = 1/(2*radians(a)**2)
    #print("P_2", P_2(a), "difference from integral", P_2(a) - P_2_numerical(a))
    print("a=%g, sigma=%g, P_2=%g, P_2_inv(P_2(a))-a=%g"
          % (a, sigma, P_2(a), P_2_inv(P_2(a))-a))