def test_mixed_vector_length(): #================================================================ "Check the definition of scalar nonlinear parameters and vector linear parameters" model = Model(gauss2_scaled) model.addlinear('gaussian', vec=100) assert model.Nparam == 101
def test_mixed_vector_names(): #================================================================ "Check the definition of scalar nonlinear parameters and vector linear parameters" model = Model(gauss2_scaled) model.addlinear('gaussian', vec=100) assert 'gaussian' in model.__dict__ and 'scale' in model.__dict__
def test_freeze_vec_outofbounds(): #================================================================ "Check that a parameter cannot be frozen outside of the bounds" model = Model(gauss2_identity) model.addlinear('gaussian', vec=100, lb=0) with pytest.raises(ValueError): model.gaussian.freeze(np.full(100, -10))
def test_addlinear_set(): #================================================================ "Check that attributes of the linear parameters are editable" model = Model(gauss2_design) model.addlinear('amp1') model.amp1.set(lb=0, ub=10) assert getattr(model.amp1, 'lb') == 0 and getattr(model.amp1, 'ub') == 10
def test_addlinear_names(): #================================================================ "Check that linear parameters can be properly added" model = Model(gauss2_design) model.addlinear('amp1', lb=0) model.addlinear('amp2', lb=0) assert 'amp1' in model.__dict__ and 'amp2' in model.__dict__
def test_addlinear_length(): #================================================================ "Check that the model is contructed correctly with the appropiate number of parameters" model = Model(gauss2_design) model.addlinear('amp1', lb=0) model.addlinear('amp2', lb=0) assert model.Nparam == 6
def test_addlinear_vector_set(): #================================================================ "Check that attributes of the vector linear parameters are editable" model = Model(gauss2_identity) model.addlinear('gaussian', vec=100) model.gaussian.set(lb=np.zeros(100)) assert np.allclose(getattr(model.gaussian, 'lb'), np.zeros(100))
def test_mixed_vector_call_positional(): #================================================================ "Check that calling the model with scalar and vector parameters returns the correct response" model = Model(gauss2_scaled) model.addlinear('gaussian', vec=len(x)) reference = 5 * gauss2(3, 4, 0.2, 0.3, 0.5, 0.4) response = model(5, gauss2(3, 4, 0.2, 0.3, 0.5, 0.4)) assert np.allclose(response, reference)
def test_addlinear_call_mixed(): #================================================================ "Check that calling the model with parameters returns the correct response" model = Model(gauss2_design) model.addlinear('amp1') model.addlinear('amp2') response = model(3, 4, 0.2, width2=0.3, amp1=0.5, amp2=0.4) reference = gauss2(3, 4, 0.2, width2=0.3, amp1=0.5, amp2=0.4) assert np.allclose(response, reference)
def test_addlinear_vector_call_keywords(): #================================================================ "Check that calling the model with scalar and vector parameters returns the correct response" model = Model(gauss2_identity) model.addlinear('gaussian', vec=len(x)) reference = gauss2(mean1=3, mean2=4, width1=0.2, width2=0.3, amp1=0.5, amp2=0.4) response = model(gaussian=reference) assert np.allclose(response, reference)
def _getmodel_axis(type, vec=50): if type == 'parametric': model = Model(gauss2_axis, constants='axis') model.mean1.set(lb=0, ub=10, par0=2) model.mean2.set(lb=0, ub=10, par0=4) model.width1.set(lb=0.01, ub=5, par0=0.2) model.width2.set(lb=0.01, ub=5, par0=0.2) model.amp1.set(lb=0, ub=5, par0=1) model.amp2.set(lb=0, ub=5, par0=1) elif type == 'semiparametric': model = Model(gauss2_design_axis, constants='axis') model.mean1.set(lb=0, ub=10, par0=2) model.mean2.set(lb=0, ub=10, par0=4) model.width1.set(lb=0.01, ub=5, par0=0.2) model.width2.set(lb=0.01, ub=5, par0=0.2) model.addlinear('amp1', lb=0, ub=5) model.addlinear('amp2', lb=0, ub=5) elif type == 'semiparametric_vec': model = Model(gauss2_design_axis, constants='axis') model.mean1.set(lb=0, ub=10, par0=2) model.mean2.set(lb=0, ub=10, par0=4) model.width1.set(lb=0.01, ub=5, par0=0.2) model.width2.set(lb=0.01, ub=5, par0=0.2) model.addlinear('amps', lb=0, ub=5, vec=2) elif type == 'nonparametric': model = Model(lambda x: gauss2_design_axis(x, 3, 4, 0.5, 0.2), constants='x') model.addlinear('amp1', lb=0) model.addlinear('amp2', lb=0) elif type == 'nonparametric_vec': model = Model(lambda x: np.eye(len(x)), constants='x') model.addlinear('dist', lb=0, vec=vec) return model
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 _getmodel(type): if type == 'parametric': model = Model(gauss2) model.mean1.set(lb=0, ub=10, par0=2) model.mean2.set(lb=0, ub=10, par0=4) model.width1.set(lb=0.1, ub=5, par0=0.2) model.width2.set(lb=0.1, ub=5, par0=0.2) model.amp1.set(lb=0, ub=5, par0=1) model.amp2.set(lb=0, ub=5, par0=1) elif type == 'semiparametric': model = Model(gauss2_design) model.mean1.set(lb=0, ub=10, par0=2) model.mean2.set(lb=0, ub=10, par0=4) model.width1.set(lb=0.1, ub=5, par0=0.2) model.width2.set(lb=0.1, ub=5, par0=0.2) model.addlinear('amp1', lb=0, ub=5) model.addlinear('amp2', lb=0, ub=5) elif type == 'nonparametric': model = Model(gauss2_design(3, 4, 0.5, 0.2)) model.addlinear('amp1', lb=0) model.addlinear('amp2', lb=0) return model
ub=20, par0=4.5, units='nm') dd_gauss2.width1.set(description='1st Gaussian standard deviation', lb=0.05, ub=2.5, par0=0.2, units='nm') dd_gauss2.width2.set(description='2nd Gaussian standard deviation', lb=0.05, ub=2.5, par0=0.2, units='nm') dd_gauss2.addlinear('amp1', description='1st Gaussian amplitude', lb=0, par0=1, units='') dd_gauss2.addlinear('amp2', description='2nd Gaussian amplitude', lb=0, par0=1, units='') # Add documentation dd_gauss2.__doc__ = _dd_docstring(dd_gauss2, notes) + docstr_example('dd_gauss2') #======================================================================================= # dd_gauss3 #======================================================================================= ntoes = r"""
model2 = dl.dd_rice model = lincombine(model1, model2) x = np.linspace(0, 10, 400) truth = model1(x, 3, 0.2) + model2(x, 4, 0.5) model.mean_1.par0 = 3 model.location_2.par0 = 4 result = fit(model, truth, x, x) assert np.allclose(result.model, truth) # ====================================================================== model_vec = Model(lambda r: np.eye(len(r)), constants='r') model_vec.addlinear('Pvec', vec=100, lb=0) # ====================================================================== def test_vec_Nparam_nonlin(): "Check that the combined model with a vector-form parameter has the right number of parameters" model1 = dl.dd_gauss model2 = model_vec model = lincombine(model1, model2) assert model.Nnonlin == model1.Nnonlin + model2.Nnonlin # ======================================================================
def test_addlinear_vector_names(): "Check that linear parameters can be defined as vectors" model = Model(gauss2_identity) model.addlinear('gaussian', vec=100) assert 'gaussian' in model.__dict__
model = dl.dd_gauss2 linkedmodel = link(model, amp=['amp1', 'amp2']) x = np.linspace(0, 10, 400) ref = model(x, 4, 0.5, 2, 0.2, 0.9, 0.9) response = linkedmodel(x, 4, 0.5, 2, 0.2, 0.9) assert np.allclose(response, ref) # ====================================================================== double_vec = Model(lambda shift, scale: shift + scale * np.eye(80)) double_vec.addlinear('vec1', vec=40, lb=0, par0=0) double_vec.addlinear('vec2', vec=40, lb=0, par0=0) # ====================================================================== def test_vec_link_name(): "Check that the parameters are linked to the proper name" model = double_vec linkedmodel = link(model, vec=['vec1', 'vec2']) assert hasattr(linkedmodel, 'vec') and not hasattr( linkedmodel, 'vec1') and not hasattr(linkedmodel, 'vec2') # ======================================================================
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
def test_addlinear_vector_length(): "Check that linear parameters can be defined as vectors" model = Model(gauss2_identity) model.addlinear('gaussian', vec=100) assert model.Nparam == 100