diff --git a/.github/workflows/ci_PR.yml b/.github/workflows/ci_PR.yml index 1a4ac9c1b..ba5b6296e 100644 --- a/.github/workflows/ci_PR.yml +++ b/.github/workflows/ci_PR.yml @@ -5,55 +5,32 @@ on: - "**" jobs: - build: + tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.8] - steps: - - uses: actions/checkout@v2 + python-version: [3.9] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v2 - if: startsWith(runner.os, 'Windows') - with: - path: | - ~\pipwin - ~\AppData\Local\pip\Cache - key: ${{ runner.os }}-${{ hashFiles('**/setup.py') }} - restore-keys: | - {{ runner.os }}-pip- - - - uses: actions/cache@v2 - if: startsWith(runner.os, 'macOS') - with: - path: | - ~/Library/Caches/pip - key: ${{ runner.os }}-${{ hashFiles('**/setup.py') }} - restore-keys: | - {{ runner.os }}-pip- - - - uses: actions/cache@v2 - if: startsWith(runner.os, 'Linux') - with: - path: | - ~/.cache/pip - key: ${{ runner.os }}-${{ hashFiles('**/setup.py') }} - restore-keys: | - {{ runner.os }}-pip- - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'false' run: | python -m pip install --upgrade pip - python setup.py install - python -m pip install pytest - + pip install . + + - name: Windows MKL linking + if: matrix.os == 'windows-latest' + run: | + python upgrade_mkl.py + - name: Test with pytest - run: pytest + run: | + pytest diff --git a/.github/workflows/ci_scheduled.yml b/.github/workflows/ci_scheduled.yml index 75f04be47..61117fab6 100644 --- a/.github/workflows/ci_scheduled.yml +++ b/.github/workflows/ci_scheduled.yml @@ -6,7 +6,7 @@ on: - cron: '30 6 * * 1' jobs: - build: + matrix_test: runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -25,7 +25,6 @@ jobs: if: startsWith(runner.os, 'Windows') with: path: | - ~\pipwin ~\AppData\Local\pip\Cache key: ${{ runner.os }}-${{ hashFiles('**/setup.py') }} restore-keys: | @@ -54,7 +53,6 @@ jobs: run: | python -m pip install --upgrade pip python setup.py install - python -m pip install pytest - name: Test with pytest run: pytest diff --git a/.github/workflows/package_upload.yml b/.github/workflows/package_upload.yml index 45bbb7291..f30bd62db 100644 --- a/.github/workflows/package_upload.yml +++ b/.github/workflows/package_upload.yml @@ -23,10 +23,12 @@ jobs: - name: Build distribution run: | python setup.py sdist + ./setup.py bdist_wheel - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} conda-build-n-publish: runs-on: ubuntu-latest diff --git a/VERSION b/VERSION index 64a3b7907..b0d2e474f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.14.1 +v0.14.2 diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 913b9c1e4..b8f26b3e4 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -20,17 +20,17 @@ requirements: - numpy - scipy - cvxopt + - dill + - pytest about: home: https://jeschkelab.github.io/DeerLab/index.html license: MIT summary: 'Comprehensive package for data analysis of dipolar EPR spectroscopy' description: | - DeerLab is a Python package for the analysis of dipolar EPR (electron paramagnetic resonance) - spectroscopy data. Dipolar EPR spectroscopy techniques include DEER (double electron-electron resonance), - RIDME (relaxation-induced dipolar modulation enhancement), and others. - DeerLab consists of a collection of functions for modelling, data processing, and least-squares fitting. - They can be combined in scripts to build custom data analysis workflows. DeerLab supports both classes of - distance distribution models: non-parametric (Tikhonov regularization and related) and parametric - (multi-Gaussians etc). It also provides a selection of background and experiment models. + DeerLab is a comprehensive free scientific software package for Python focused on modelling, + penalized least-squares regression, and uncertainty quantification. It provides highly + specialized on the analysis of dipolar EPR (electron paramagnetic resonance) spectroscopy data. + Dipolar EPR spectroscopy techniques include DEER (double electron-electron resonance), + RIDME (relaxation-induced dipolar modulation enhancement), and others. dev_url: https://jeschkelab.github.io/DeerLab diff --git a/deerlab/bootstrap_analysis.py b/deerlab/bootstrap_analysis.py index c349a3594..368571a3a 100644 --- a/deerlab/bootstrap_analysis.py +++ b/deerlab/bootstrap_analysis.py @@ -9,8 +9,9 @@ from joblib import Parallel, delayed from deerlab.classes import UQResult from deerlab.utils import isnumeric +from deerlab.noiselevel import noiselevel -def bootstrap_analysis(fcn,Vexp,Vfit, samples=1000, resampling='gaussian', verbose = False, cores=1, memorylimit=8): +def bootstrap_analysis(fcn,Vexp,Vfit, samples=1000, noiselvl=None, resampling='gaussian', verbose = False, cores=1, memorylimit=8): r""" Bootstrap analysis for uncertainty quantification @@ -33,6 +34,11 @@ def bootstrap_analysis(fcn,Vexp,Vfit, samples=1000, resampling='gaussian', verbo results improve with the number of boostrap samples evaluated, the default is 1000. + noiselvl : scalar or list thereof + Noise level of the input dataset(s), specified as standard deviations. + If not specified, these are automatically estimated from the experimental + dataset(s). + resampling : string, optional Specifies the method employed for re-sampling new bootstrap samples. @@ -94,10 +100,11 @@ def myfcn(V): raise KeyError('The 1st argument must be a callable function accepting dataset(s) as input.') # Get residuals and estimate standard deviation - residuals,sigma = ([],[]) for i in range(nSignals): - residuals.append(Vfit[i] - Vexp[i]) - sigma.append(np.std(residuals[i])) + residuals = [Vfit[i] - Vexp[i] for i in range(nSignals)] + if noiselvl is None: + noiselvl = [noiselevel(Vexp[i]) for i in range(nSignals)] + noiselvl = np.atleast_1d(noiselvl) # Prepare bootstrap sampler (reduced I/O-stream when parallelized) def sample(): @@ -106,7 +113,7 @@ def sample(): #Determine requested re-sampling method if resampling == 'gaussian': # Resample from a Gaussian distribution with variance estimated from the residuals - Vsample[i] = Vfit[i] + np.random.normal(0, sigma[i], len(Vfit[i])) + Vsample[i] = Vfit[i] + np.random.normal(0, noiselvl[i], len(Vfit[i])) elif resampling == 'residual': # Resample from the residual directly Vsample[i] = Vfit[i] + residuals[i][np.random.permutation(len(Vfit[i]))] diff --git a/deerlab/dd_models.py b/deerlab/dd_models.py index a638ecea2..6539fa3e3 100644 --- a/deerlab/dd_models.py +++ b/deerlab/dd_models.py @@ -140,7 +140,7 @@ def _nonparametric(): 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',unit="nm⁻¹", normalization=lambda P: P/np.trapz(P,r)) + dd_nonparametric.addlinear('P',vec=len(r),lb=0,par0=0,description='Non-parametric distance distribution',unit="nm⁻¹", normalization=lambda P: _normalize(r,P)) return dd_nonparametric diff --git a/deerlab/dipolarmodel.py b/deerlab/dipolarmodel.py index 0d6eb044a..3d126dede 100644 --- a/deerlab/dipolarmodel.py +++ b/deerlab/dipolarmodel.py @@ -178,7 +178,7 @@ def dipolarpathways(*param): if param in model._parameter_list(order='vector'): subset[getattr(model,param).idx] = idx - kernelmethod = 'fresnel' if orisel is None else 'grid' + kernelmethod = 'fresnel' if orisel is None and np.isinf(excbandwidth) else 'grid' #------------------------------------------------------------------------ def Vnonlinear_fcn(*nonlin): @@ -243,9 +243,6 @@ def Vnonlinear_fcn(*nonlin): # Set other dipolar model specific attributes DipolarSignal.description = 'Dipolar signal model' - DipolarSignal.Pmodel = Pmodel - DipolarSignal.Bmodel = Pmodel - DipolarSignal.Npathways = npathways return DipolarSignal #=============================================================================== @@ -293,7 +290,8 @@ def dipolarpenalty(Pmodel, r, type, selection=None): # Define the compactness penalty function def compactness_penalty(*args): P = Pmodel(*[r]*Nconstants,*args) - P = P/np.trapz(P,r) + if not np.all(P==0): + P = P/np.trapz(P,r) return np.sqrt(P*(r - np.trapz(P*r,r))**2*np.mean(np.diff(r))) # Add the penalty to the Pmodel penalty = Penalty(compactness_penalty,selection, @@ -690,4 +688,4 @@ def ex_ridme(tau1, tau2, pathways=None): harmonics = [harmonics[pathway-1] for pathway in pathways] return ExperimentInfo('RIDME',reftimes,harmonics) -#=============================================================================== \ No newline at end of file +#=============================================================================== diff --git a/deerlab/diststats.py b/deerlab/diststats.py index 6eb327a53..982657b81 100644 --- a/deerlab/diststats.py +++ b/deerlab/diststats.py @@ -95,7 +95,7 @@ def analyze_rmode(V): # Auxiliary functions # ------------------- - int = np.trapz(P,r) + int = np.trapz(P,r) if not np.all(P==0) else 1 def normalize(P): return P/int # Percentile function diff --git a/deerlab/model.py b/deerlab/model.py index 422e6bc70..d309d49fa 100644 --- a/deerlab/model.py +++ b/deerlab/model.py @@ -1135,7 +1135,7 @@ def bootstrap_fcn(ysim): if not isinstance(fit.model,list): fit.model = [fit.model] return (fit.param,*fit.model) # Bootstrapped uncertainty quantification - param_uq = bootstrap_analysis(bootstrap_fcn,ysplit,fitresults.model,samples=bootstrap) + param_uq = bootstrap_analysis(bootstrap_fcn,ysplit,fitresults.model,samples=bootstrap,noiselvl=noiselvl) # Include information on the boundaries for better uncertainty estimates paramlb = model._vecsort(model._getvector('lb'))[np.concatenate(param_idx)] paramub = model._vecsort(model._getvector('ub'))[np.concatenate(param_idx)] diff --git a/deerlab/selregparam.py b/deerlab/selregparam.py index 1558a606d..9a63d9657 100644 --- a/deerlab/selregparam.py +++ b/deerlab/selregparam.py @@ -151,22 +151,22 @@ def register_ouputs(optout): # L-curve minimum-radius method (LR) if method == 'lr': - Eta = np.log(penalties) - Rho = np.log(residuals) + Eta = np.log(np.asarray(penalties)+1e-20) + Rho = np.log(np.asarray(residuals)+1e-20) dd = lambda x: (x-np.min(x))/(np.max(x)-np.min(x)) functional = dd(Rho)**2 + dd(Eta)**2 - + # L-curve maximum-curvature method (LC) elif method == 'lc': - d1Residual = np.gradient(np.log(residuals)) + d1Residual = np.gradient(np.log(np.asarray(residuals)+1e-20)) d2Residual = np.gradient(d1Residual) - d1Penalty = np.gradient(np.log(penalties)) + d1Penalty = np.gradient(np.log(np.asarray(penalties)+1e-20)) d2Penalty = np.gradient(d1Penalty) functional = (d1Residual*d2Penalty - d2Residual*d1Penalty)/(d1Residual**2 + d1Penalty**2)**(3/2) + functional = -functional # Maximize instead of minimize # Find minimum of the selection functional alphaOpt = alphaCandidates[np.argmin(functional)] - else: raise KeyError("Search method not found. Must be either 'brent' or 'grid'.") diff --git a/docsrc/source/_templates/nav.html b/docsrc/source/_templates/nav.html index 39c3ae907..87e835772 100644 --- a/docsrc/source/_templates/nav.html +++ b/docsrc/source/_templates/nav.html @@ -1,3 +1,4 @@ + {%- set userguide_dropdown_navigation = [ ('Basics', pathto('basics')), ('Getting Started', pathto('getting_started')), @@ -5,7 +6,6 @@ ('Fitting', pathto('dipolar_guide_fitting')), ('Theory', pathto('theory')),] -%} - {%- set advancedguide_dropdown_navigation = [ ('Modelling Guide', pathto('modelling_guide')), @@ -23,18 +23,18 @@ -%}
- -