Esempio n. 1
0
def test_multipath_renorm():
    #==================================================================================
    "Check that multi-pathway kernels are properly generated without renormalization"

    t1 = np.linspace(0, 10, 300)
    conc = 50
    t2 = 0.3
    tau1 = 4.24
    tau2 = 4.92
    t = (tau1 + tau2) - (t1 + t2)
    prob = 0.8
    lam = [prob**2, prob * (1 - prob)]
    T0 = [0, tau2 - t2]
    Bmodel = lambda t, lam: bg_hom3d(t, conc, lam)

    # Reference
    Bref = 1
    for p in range(len(lam)):
        Bref = Bref * Bmodel((t - T0[p]), lam[p])

    paths = []
    paths.append(1 - prob)
    paths.append([prob**2, 0])
    paths.append([prob * (1 - prob), tau2 - t2])

    # Output
    B = dipolarbackground(t, paths, Bmodel)

    assert max(abs(B - Bref)) < 1e-8
Esempio n. 2
0
def test_basic():
    #==================================================================================
    "Basic functionality test on dipolarbackground"

    t = np.linspace(0, 5, 150)
    conc = 50
    lam = 0.5

    #Reference
    Bref = bg_hom3d(t, conc, lam)

    #Output
    Bmodel = lambda t, lam: bg_hom3d(t, conc, lam)
    path = [[], []]
    path[0] = [1 - lam]
    path[1] = [lam, 0]
    B = dipolarbackground(t, path, Bmodel)

    assert max(abs(B - Bref) < 1e-8)
Esempio n. 3
0
def test_harmonics():
    #==================================================================================
    "Check that one can generate a background with multiple higher pathway harmonics"

    t = np.linspace(0, 5, 150)
    conc = 50
    lam = 0.5
    n = 2
    #Reference
    Bref = bg_hom3d(n * t, conc, lam)

    #Output
    Bmodel = lambda t, lam: bg_hom3d(t, conc, lam)
    path = [[], []]
    path[0] = [1 - lam]
    path[1] = [lam, 0, 2]
    B = dipolarbackground(t, path, Bmodel)

    assert max(abs(B - Bref)) < 1e-8
Esempio n. 4
0
def test_singletime():
    #==================================================================================
    "Check that one can generate a background with one single time-domain point"

    t = 2.5
    conc = 50
    lam = 0.5

    #Reference
    Bref = bg_hom3d(t, conc, lam)

    #Output
    Bmodel = lambda t, lam: bg_hom3d(t, conc, lam)
    path = [[], []]
    path[0] = [1 - lam]
    path[1] = [lam, 0]
    B = dipolarbackground(t, path, Bmodel)

    assert max(abs(B - Bref) < 1e-8)
Esempio n. 5
0
def test_phenomenological():
    #==================================================================================
    "Check that phenomenological background models are propely used"

    t = np.linspace(0, 5, 150)
    kappa = 0.3
    lam = 0.5

    #Reference
    Bref = bg_exp(t, 0.3)

    #Output
    Bmodel = lambda t: bg_hom3d(t, kappa, 1)
    path = [[], []]
    path[0] = [1 - lam]
    path[1] = [lam, 0]
    B = dipolarbackground(t, path, Bmodel)

    assert max(abs(B - Bref) < 1e-8)
Esempio n. 6
0
def test_physical():
    #==================================================================================
    "Check that physical background models are propely used"

    t = np.linspace(0, 5, 150)
    conc = 50
    lam = 0.5

    #Reference
    Bref = bg_hom3d(t, conc, lam)

    #Output
    Bmodel = lambda t, lam: bg_hom3d(t, conc, lam)
    path = [[], []]
    path[0] = [1 - lam]
    path[1] = [lam, 0]
    B = dipolarbackground(t, path, Bmodel)

    assert max(abs(B - Bref) < 1e-8)
Esempio n. 7
0
def dipolarkernel(t,
                  r,
                  *,
                  pathways=None,
                  mod=None,
                  bg=None,
                  method='fresnel',
                  excbandwidth=inf,
                  orisel=None,
                  g=[ge, ge],
                  integralop=True,
                  nKnots=5001,
                  clearcache=False,
                  memorylimit=8):
    #===================================================================================================
    r"""Compute the dipolar kernel operator which enables the linear transformation from
    distance-domain to time-domain data. 

    Parameters
    ----------
    t : array_like
        Dipolar time axis, in microseconds.
    
    r : array_like
        Distance axis, in nanometers.
    
    pathways : list of lists  or ``None``, optional
        List of pathways. Each pathway is defined as a list of the pathway's amplitude (lambda), refocusing time in microseconds (T0), 
        and harmonic (n), i.e. ``[lambda, T0, n]`` or ``[lambda, T0]``. If n is not given, it is assumed to be 1. 
        For a unmodulated pathway, specify only the amplitude, i.e. ``[Lambda0]``. If neither ``pathways`` or ``mod`` are specified
        (or ``None``), full-modulation is assumed ``pathways=[[1,0]]``.
    
    mod : scalar  or ``None``, optional
        Modulation depth for the simplified 4-pulse DEER model. If neither ``pathways`` or ``mod`` are specified (or ``None``),
        it is assumed to be ``mod=1``. 
        
    bg : callable or array_like or ``None``, optional
        For a single-pathway model, the numerical background decay can be passed as an array. 
        For multiple pathways, a callable function must be passed, accepting a time-axis array as first input and a pathway amplitude as a second, i.e. ``B = lambda t,lam: bg_model(t,par,lam)``. If set to ``None``, no background decay is included.

    method : string, optional
        Numerical method for kernel matrix calculation: 

            * ``'fresnel'`` - uses Fresnel integrals for the kernel (fast, accurate)
            * ``'integral'`` - uses explicit integration function (slow, accurate)
            * ``'grid'`` - powder average via explicit grid integration (slow, inaccurate)
        
        The default is ``'fresnel'``.

    orisel : callable  or ``None``, optional 
        Probability distribution of possible orientations of the interspin vector to account for orientation selection. Must be 
        a function taking a value of the angle θ∈[0,π/2] between the interspin vector and the external magnetic field and returning
        the corresponding probability density. If specified as ``None`` (by default), a uniform distribution is assumed. 
        Requires the ``'grid'`` or ``'integral'`` methods.

    excbandwidth : scalar, optional
        Excitation bandwidth of the pulses in MHz to account for limited excitation bandwidth [4]_.
        Requires the ``'grid'`` or ``'integral'`` methods.

    g : scalar, 2-element array, optional
        Electron g-values of the spin centers ``[g1, g2]``. If a single g is specified, ``[g, g]`` is assumed 
    
    integralop : boolean, optional
        Whether to return K as an integral operator (i.e ``K = K*dr``) or not (``K``). Usage as an integral operator means that the 
        matrix operation ``V=K@P`` with a normalized distance distribution (i.e. ``trapz(r,P)==1``) leads to a signal ``V`` with 
        ampliude ``V(t=0)=1``. Enabled by default.
    
    nKnots : scalar, optional
        Number of knots for the grid of powder orientations to be used in the ``'grid'`` kernel calculation method. By default set to 5001 knots.
        
    clearcache : boolean, optional
        Clear the cached dipolar kernels at the beginning of the function to save memory. Disabled by default.

    memorylimit : 
        Memory limit to be allocated for the dipolar kernel. If the requested kernel exceeds this limit, the 
        execution will be stopped. The default is 12GB.  

    Returns
    --------
    K : ndarray
        Dipolar kernel operator, such that for a distance distribution ``P``, the dipolar signal is ``V = K@P``

    Notes
    -----
    For a multi-pathway DEER [1]_ signal (e.g, 4-pulse DEER with 2+1 contribution 5-pulse DEER with 4-pulse DEER residual signal, and more complicated experiments), ``pathways`` contains a list of pathway amplitudes and refocusing times (in microseconds).
    The background function specified as ``B`` is used as basis function, and the actual multipathway background included into the kernel is compued using :ref:`dipolarbackground`. The background in included in the dipolar kernel definition [2]_. 
    Optionally, the harmonic (1 = fundamental, 2 = first overtone, etc.) can be given as a third value in each row. This can be useful for modeling RIDME signals [3]_. If not given, the harmonic is 1 for all pathways. 


    Examples
    --------
    To specify the standard model for 4-pulse DEER with an unmodulated offset and a single dipolar pathway that refocuses at time 0, use::
    
        lam = 0.4  # modulation depth main signal
        pathways = [[1-lam], [lam, 0]]
        
        K = dl.dipolarkernel(t,r,pathways=pathways)


    A shorthand input syntax equivalent to this input::

        lam = 0.4
        K = dl.dipolarkernel(t,r,mod=lam)


	To specify a more complete 4-pulse DEER model that, e.g includes the 2+1 contribution, use::

        Lam0 = 0.5  # unmodulated part
        lam = 0.4  # modulation depth main signal
        lam21 = 0.1  # modulation depth 2+1 contribution
        tau2 = 4  # refocusing time (µs) of 2+1 contribution

        path0 = Lam0  # unmodulated pathway
        path1 = [lam1, 0]  # main dipolar pathway, refocusing at time zero
        path1 = [lam2, tau2]  # 2+1 dipolar pathway, refocusing at time tau2
        pathways = [path0, path1, path2]  
        
        K = dl.dipolarkernel(t,r,pathways=pathways)


    References
    ----------
    .. [1] L. Fábregas Ibáñez, G. Jeschke, and S. Stoll. 
        DeerLab: A comprehensive toolbox for analyzing dipolar EPR spectroscopy data, Magn. Reson., 1, 209–224, 2020 

    .. [2] L. Fábregas Ibáñez, and G. Jeschke
        Optimal background treatment in dipolar spectroscopy, Physical Chemistry Chemical Physics, 22, 1855–1868, 2020.

    .. [3] K. Keller, V. Mertens, M. Qi, A. I. Nalepa, A. Godt, A. Savitsky, G. Jeschke, and M. Yulikov
        Computing distance distributions from dipolar evolution data with overtones: RIDME spectroscopy with Gd(III)-based spin labels, Physical Chemistry Chemical Physics, 19

    .. [4] J. E. Banham, C. M. Baker, S. Ceola, I. J. Day, G.H. Grant, E. J. J. Groenen, C. T. Rodgers, G. Jeschke, C. R. Timmel
        Distance measurements in the borderline region of applicability of CW EPR and DEER: A model study on a homologous series of spin-labelled peptides, Journal of Magnetic Resonance, 191, 2, 2008, 202-218
    """
    # Clear cache of memoized function is requested
    if clearcache:
        elementarykernel.cache_clear()
        _Cgrid.cache_clear()

    # Ensure that inputs are Numpy arrays
    r, t, g = np.atleast_1d(r, t, g)

    memory_requirement = 8 * len(t) * len(r) / 1e9  # GB
    if memory_requirement > memorylimit:
        raise MemoryError(
            f'The requested kernel requires {memory_requirement:.2f}GB, exceeding the current memory limit {memorylimit:.2f}GB.'
        )

    # Check validity of some inputs
    if len(g) == 1:
        g = [g, g]
    elif len(g) != 2:
        raise TypeError(
            'The g-value must be specified as a scalar or a two-element array.'
        )

    if np.any(r <= 0):
        raise ValueError("All elements in r must be nonnegative and nonzero.")

    # Check that the kernel construction method is compatible with requested options
    if method == 'fresnel':
        if excbandwidth != inf:
            raise KeyError(
                "Excitation bandwidths can only be specified with the 'grid' or 'integral' methods."
            )
        if orisel is not None:
            raise KeyError(
                "Orientation selection weights can only be specified with  the 'grid' or 'integral' methods."
            )

    # Check whether the full pathways or the modulation depth have been passed
    pathways_passed = pathways is not None
    moddepth_passed = mod is not None
    if pathways_passed and moddepth_passed:
        raise KeyError(
            "Modulation depth and dipolar pathways cannot be specified simultaneously."
        )

    if moddepth_passed:
        # Construct 4-pulse DEER pathways if modulation depth is specified
        pathways = [[1 - mod], [mod, 0]]
    elif not pathways_passed:
        # Full modulation if neither pathways or modulation depth are specified
        pathways = [[1, 0]]

    # Ensure correct data types of the pathways and its components
    paths = [[np.atleast_1d(p).astype(float) for p in path]
             for path in pathways]
    paths = [
        np.concatenate([np.atleast_1d(p) for p in path]).astype(float)
        for path in paths
    ]

    # Get unmodulated pathways
    unmodulated = [
        paths.pop(i) for i, path in enumerate(paths) if len(path) == 1
    ]
    # Combine all unmodulated contributions
    if unmodulated:
        Λ0 = sum(np.concatenate([path for path in unmodulated]))
    else:
        Λ0 = 0

    # Check structure of pathways
    for i, path in enumerate(paths):
        if len(path) == 2:
            # If harmonic is not defined, append default n=1
            paths[i] = np.append(path, 1)
        elif len(path) != 3:
            # Otherwise paths are not correctly defined
            raise KeyError(
                f'The pathway #{i} must be a list of two or three elements [λ, T0] or [λ, T0, n]'
            )

    # Define kernel matrix auxiliary function
    K0 = lambda t: elementarykernel(t, r, method, excbandwidth, nKnots, g,
                                    orisel)

    # Build dipolar kernel matrix, summing over all pathways
    K = Λ0
    for pathway in paths:
        λ, T0, n = pathway
        K += λ * K0(n * (t - T0))

    # Multiply by background if given
    if bg is not None:
        if type(bg) is types.LambdaType:
            B = dipolarbackground(t, pathways, bg)
        else:
            B = np.atleast_1d(bg)
        K *= B[:, np.newaxis]

    # Include delta-r factor for integration
    if integralop and len(r) > 1:

        # Vectorized matrix form of trapezoidal integration method
        dr = np.zeros_like(r)
        dr[0] = r[1] - r[0]
        dr[-1] = r[-1] - r[-2]
        dr[1:-1] = r[2:] - r[0:-2]
        dr = dr / 2
        K = K * dr

    return K