Example #1
0
def freedist(r):
    def _nonparametric():
        return np.eye(len(r))

    # Create model
    dd_nonparametric = Model(_nonparametric, constants='r')
    dd_nonparametric.description = 'Non-parametric distribution model'
    # Parameters
    dd_nonparametric.addlinear(
        'P',
        vec=len(r),
        lb=0,
        par0=0,
        description='Non-parametric distance distribution')
    return dd_nonparametric
Example #2
0
def dipolarmodel(t,
                 r,
                 Pmodel=None,
                 Bmodel=bg_hom3d,
                 npathways=1,
                 harmonics=None,
                 experiment=None,
                 excbandwidth=np.inf,
                 orisel=None,
                 g=[ge, ge]):
    """
    Construct a dipolar EPR signal model. 

    Parameters
    ----------
    t : array_like 
        Vector of dipolar time increments, in microseconds.
    r : array_like 
        Vector of intraspin distances, in nanometers.
    Pmodel : :ref:`Model`, optional 
        Model for the distance distribution. If not speficied, a non-parametric
        distance distribution is assumed. 
    Bmodel : :ref:`Model`, optional 
        Model for the intermolecular (background) contribution. If not specified, 
        a background arising from a homogenous 3D distribution of spins is assumed. 
    npathways : integer scalar
        Number of dipolar pathways. If not specified, a single dipolar pathway is assumed. 
    experiment : :ref:`ExperimentInfo`, optional 
        Experimental information obtained from experiment models (``ex_``). If specified, the 
        boundaries and start values of the dipolar pathways' refocusing times and amplitudes 
        will be refined based on the specific experiment's delays.
    harmonics : list of integers 
        Harmonics of the dipolar pathways. Must be a list with `npathways` harmonics for each
        defined dipolar pathway.  
    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. 
    excbandwidth : scalar, optional
        Excitation bandwidth of the pulses in MHz to account for limited excitation bandwidth.
    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 

    Returns
    -------
    Vmodel : :ref:`Model`
        Dipolar signal model object.
    """

    # Input parsing and validation
    if not isinstance(Pmodel, Model) and Pmodel is not None:
        raise TypeError('The argument Pmodel must be a valid Model object')
    if not isinstance(Bmodel, Model) and Bmodel is not None:
        raise TypeError(
            'The argument Bmodel must be a valid Model object or None.')
    if not isinstance(npathways, int) or npathways <= 0:
        raise ValueError(
            'The number of pathway must be an integer number larger than zero.'
        )
    if isinstance(harmonics, float) or isinstance(harmonics, int):
        harmonics = [int(harmonics)]
    if not isinstance(harmonics, list) and harmonics is not None:
        raise TypeError(
            'The harmonics must be specified as a list of integer values.')

    #------------------------------------------------------------------------
    def _importparameter(parameter):
        """ Private function for importing a parameter's metadata """
        return {
            'lb': parameter.lb,
            'ub': parameter.ub,
            'par0': parameter.par0,
            'description': parameter.description,
            'units': parameter.units,
            'linear': parameter.linear
        }

    #------------------------------------------------------------------------

    # Parse the harmonics of the dipolar pathways
    if harmonics is None:
        harmonics = np.ones(npathways)
    if len(harmonics) != npathways:
        raise ValueError(
            'The number of harmonics must match the number of dipolar pathways.'
        )
    #------------------------------------------------------------------------
    def dipolarpathways(*param):
        """ Parametric constructor of the dipolar pathways definition """
        param = np.atleast_1d(param)
        if npathways == 1:
            # Single-pathway model, use modulation depth notation
            lam, reftime = param
            pathways = [[1 - lam], [lam, reftime, harmonics[0]]]
        else:
            # Otherwise, use general notation
            lams = param[np.arange(0, len(param), 2)]
            reftimes = param[np.arange(1, len(param), 2)]
            Lam0 = np.maximum(0, 1 - np.sum(lams))
            # Unmodulated pathways ccontribution
            pathways = [[Lam0]]
            # Modulated pathways
            for n in range(npathways):
                pathways.append([lams[n], reftimes[n], harmonics[n]])
        return pathways

    #------------------------------------------------------------------------

    # Construct the signature of the dipolarpathways() function
    if npathways == 1:
        variables = ['mod', 'reftime']
    else:
        variables = []
        for n in range(npathways):
            variables.append(f'lam{n+1}')
            variables.append(f'reftime{n+1}')

    # Create the dipolar pathways model object
    PathsModel = Model(dipolarpathways, signature=variables)

    Pnonparametric = Pmodel is None
    if Pnonparametric:
        Pmodel = freedist(r)
    Nconstants = len(Pmodel._constantsInfo)

    # Populate the basic information on the dipolar pathways parameters
    if npathways == 1:
        # Special case: use modulation depth notation instead of general pathway amplitude
        getattr(PathsModel, f'mod').set(lb=0,
                                        ub=1,
                                        par0=0.2,
                                        description=f'Modulation depth',
                                        units='')
        getattr(PathsModel, f'reftime').set(par0=0,
                                            description=f'Refocusing time',
                                            units='μs')
    else:
        # General case: use pathway ampltiudes and refocusing times
        for n in range(npathways):
            getattr(PathsModel, f'lam{n+1}').set(
                lb=0,
                ub=1,
                par0=0.2,
                description=f'Amplitude of pathway #{n+1}',
                units='')
            getattr(PathsModel, f'reftime{n+1}').set(
                par0=0,
                lb=-20,
                ub=20,
                description=f'Refocusing time of pathway #{n+1}',
                units='μs')

    # Construct the signature of the dipolar signal model function
    signature = []
    parameters, linearparam, vecparam = [], [], []
    for model in [PathsModel, Bmodel, Pmodel]:
        if model is not None:
            for param in model._parameter_list(order='vector'):
                if np.any(getattr(model, param).linear):
                    parameters.append(getattr(model, param))
                    linearparam.append({
                        'name':
                        param,
                        'vec':
                        len(np.atleast_1d(getattr(model, param).idx))
                    })
                elif not (model == Bmodel and param == 'lam'):
                    signature.append(param)
                    parameters.append(getattr(model, param))

    # Initialize lists of indices to access subsets of nonlinear parameters
    Psubset = np.zeros(Pmodel.Nnonlin, dtype=int)
    PathsSubset = np.zeros(PathsModel.Nnonlin, dtype=int)
    if Bmodel is None:
        Bsubset = []
    else:
        Bsubset = np.zeros(Bmodel.Nnonlin -
                           ('lam' in Bmodel._parameter_list()),
                           dtype=int)

    # Determine subset indices based on main function signature
    idx = 0
    for model, subset in zip([Pmodel, Bmodel, PathsModel],
                             [Psubset, Bsubset, PathsSubset]):
        if model is not None:
            for idx, param in enumerate(signature):
                if param in model._parameter_list(order='vector'):
                    subset[getattr(model, param).idx] = idx

    kernelmethod = 'fresnel' if orisel is None else 'grid'

    #------------------------------------------------------------------------
    def Vnonlinear_fcn(*nonlin):
        """ Non-linear part of the dipolar signal function """
        # Make input arguments as array to access subsets easily
        nonlin = np.atleast_1d(nonlin)
        # Construct the basis function of the intermolecular contribution
        if Bmodel is None:
            Bfcn = np.ones_like(t)
        elif hasattr(Bmodel, 'lam'):
            Bfcn = lambda t, lam: Bmodel.nonlinmodel(
                t, *np.concatenate([nonlin[Bsubset], [lam]]))
        else:

            Bfcn = lambda t, _: Bmodel.nonlinmodel(t, *nonlin[Bsubset])
        # Construct the definition of the dipolar pathways
        pathways = PathsModel.nonlinmodel(*nonlin[PathsSubset])
        # Construct the dipolar kernel
        Kdipolar = dipolarkernel(t,
                                 r,
                                 pathways=pathways,
                                 bg=Bfcn,
                                 excbandwidth=excbandwidth,
                                 orisel=orisel,
                                 g=g,
                                 method=kernelmethod)
        # Compute the non-linear part of the distance distribution
        Pnonlin = Pmodel.nonlinmodel(*[r] * Nconstants, *nonlin[Psubset])
        # Forward calculation of the non-linear part of the dipolar signal
        Vnonlin = Kdipolar @ Pnonlin
        return Vnonlin

    #------------------------------------------------------------------------

    # Create the dipolar model object
    DipolarSignal = Model(Vnonlinear_fcn, signature=signature)

    # Add the linear parameters from the subset models
    for lparam in linearparam:
        DipolarSignal.addlinear(lparam['name'], vec=lparam['vec'])

    if Pmodel is None:
        DipolarSignal.addlinear()

    # If there are no linear paramters, add a linear scaling parameter
    if DipolarSignal.Nlin == 0:
        DipolarSignal.addlinear('scale', lb=0, par0=1)

    # Import all parameter information from the subset models
    for name, param in zip(DipolarSignal._parameter_list(order='vector'),
                           parameters):
        getattr(DipolarSignal, name).set(**_importparameter(param))

    # Set prior knowledge on the parameters if experiment is specified
    if experiment is not None:
        if not isinstance(experiment, ExperimentInfo):
            raise TypeError(
                'The experiment must be a valid deerlab.ExperimentInfo object.'
            )

        # Check that the number of requested pathways does not exceed the theoretical limit of the experiment
        maxpathways = len(experiment.reftimes)
        if npathways > maxpathways:
            raise ValueError(
                f'The {experiment.name} experiment can only have up to {maxpathways} dipolar pathways.'
            )

        # Compile the parameter names to change in the model
        if npathways > 1:
            reftime_names = [f'reftime{n+1}' for n in range(npathways)]
            lams_names = [f'lam{n+1}' for n in range(npathways)]
        else:
            reftime_names = ['reftime']
            lams_names = ['mod']

        # Specify start values and boundaries according to experimental timings
        for n in range(npathways):
            getattr(DipolarSignal,
                    reftime_names[n]).set(par0=experiment.reftimes[n]['par0'],
                                          lb=experiment.reftimes[n]['lb'],
                                          ub=experiment.reftimes[n]['ub'])
            getattr(DipolarSignal,
                    lams_names[n]).set(par0=experiment.lams[n]['par0'],
                                       lb=experiment.lams[n]['lb'],
                                       ub=experiment.lams[n]['ub'])

    # Set other dipolar model specific attributes
    DipolarSignal.description = 'Dipolar signal model'
    DipolarSignal.Pmodel = Pmodel
    DipolarSignal.Bmodel = Pmodel
    DipolarSignal.Npathways = npathways

    return DipolarSignal
Example #3
0
notes = r"""
**Model**

:math:`P(r) = \frac{1}{\sigma\sqrt{2\pi}}\exp\left(-\frac{(r-\left<r\right>)^2}{2\sigma^2}\right)`

where `\left<r\right>` is the mean distance and `\sigma` the standard deviation.
"""


def _gauss(r, mean, width):
    return _multigaussfun(r, mean, width)


# Create model
dd_gauss = Model(_gauss, constants='r')
dd_gauss.description = 'Gaussian distribution model'
# Parameters
dd_gauss.mean.set(description='Mean', lb=1.0, ub=20, par0=3.5, units='nm')
dd_gauss.width.set(description='Standard deviation',
                   lb=0.05,
                   ub=2.5,
                   par0=0.2,
                   units='nm')
# Add documentation
dd_gauss.__doc__ = _dd_docstring(dd_gauss, notes) + docstr_example('dd_gauss')

#=======================================================================================
#                                     dd_gauss2
#=======================================================================================
notes = r"""
**Model**
Example #4
0
   D = \frac{\mu_0}{4\pi}\frac{(g_\mathrm{e}\mu_\mathrm{B})^2}{\hbar}
"""


def _hom3d(t, conc, lam):
    # Units conversion
    conc = conc * 1e-6 * 1e3 * Nav  # umol/L -> mol/L -> mol/m^3 -> spins/m^3
    # Compute background function
    κ = 8 * pi**2 / 9 / m.sqrt(3)
    B = np.exp(-κ * lam * conc * D * np.abs(t * 1e-6))
    return B


# Create model
bg_hom3d = Model(_hom3d, constants='t')
bg_hom3d.description = 'Background from a homogeneous distribution of spins in a 3D medium'
# Parameters
bg_hom3d.conc.set(description='Spin concentration',
                  lb=0.01,
                  ub=5000,
                  par0=50,
                  units='μM')
bg_hom3d.lam.set(description='Pathway amplitude', lb=0, ub=1, par0=1, units='')
# Add documentation
bg_hom3d.__doc__ = _docstring(bg_hom3d, notes)

#=======================================================================================
#                                     bg_hom3d_phase
#=======================================================================================
notes = r"""
**Model**