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
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
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**
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**