Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions deerlab/bootstrap_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@

import numpy as np
import types
from tqdm.auto import tqdm
from joblib import Parallel, delayed
from joblib import delayed
from deerlab.classes import UQResult
from deerlab.utils import isnumeric
from deerlab.utils import isnumeric, _ProgressParallel
from deerlab.noiselevel import noiselevel

def bootstrap_analysis(fcn,Vexp,Vfit, samples=1000, noiselvl=None, resampling='gaussian', verbose = False, cores=1, memorylimit=8):
Expand Down Expand Up @@ -188,25 +187,5 @@ def bootsample():


#-------------------------------------------------------------------------------
class _ProgressParallel(Parallel):
"""
Patch for joblib.Parallel

Overrides the print_progress() method to enable the synchronous use of the TQDM bar
even for parallel processing.
"""
def __init__(self, use_tqdm=True, total=None, *args, **kwargs):
self._use_tqdm = use_tqdm
self._total = total
super().__init__(*args, **kwargs)

def __call__(self, *args, **kwargs):
with tqdm(disable=not self._use_tqdm, total=self._total) as self._pbar:
return Parallel.__call__(self, *args, **kwargs)

def print_progress(self):
if self._total is None:
self._pbar.total = self.n_dispatched_tasks
self._pbar.n = self.n_completed_tasks
self._pbar.refresh()
#-------------------------------------------------------------------------------
21 changes: 13 additions & 8 deletions deerlab/profile_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from scipy.stats import chi2
from deerlab import fit
from deerlab import noiselevel,UQResult
from deerlab.utils import _ProgressParallel
import warnings
from tqdm import tqdm
from joblib import delayed

def profile_analysis(model,y, *args, parameters='all', grids=None, samples=50, noiselvl=None, verbose=False,**kargs):
def profile_analysis(model,y, *args, parameters='all', grids=None, samples=50, noiselvl=None, verbose=False, cores=1, **kargs):
r"""
Profile likelihood analysis for uncertainty quantification

Expand Down Expand Up @@ -49,6 +51,11 @@ def profile_analysis(model,y, *args, parameters='all', grids=None, samples=50, n
Specifies whether to print the progress of the bootstrap analysis on the
command window, the default is false.

cores : scalar, optional
Number of CPU cores/processes for parallel computing. If ``cores=1`` no parallel
computing is used. If ``cores=-1`` all available CPUs are used. The default is
one core (no parallelization).

kargs : keyword-argument pairs
Any other keyword-argument pairs to be passed to the ``fit`` function. See the
documentation of the ``fit`` function for further details.
Expand Down Expand Up @@ -106,22 +113,20 @@ def profile_analysis(model,y, *args, parameters='all', grids=None, samples=50, n
tqdm.write(f"Profiling model parameter '{parameter}':",end='')

# Calculate the profile objective function for the parameter
profile = np.zeros(len(grid))
for n,value in enumerate(tqdm(grid, disable=not verbose)):

def profile_sample(value):
# Freeze the model parameter at current value
getattr(model, parameter).freeze(value)

# Optimize the rest
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fitresult_ = fit(model, y, *args, **kargs)

# Extract the objective function value
profile[n] = fitresult_.cost

cost = fitresult_.cost
# Unfreeze the parameter
getattr(model, parameter).unfreeze()
return cost

profile = _ProgressParallel(n_jobs=cores,total=len(grid),use_tqdm=verbose)(delayed(profile_sample)(value) for value in grid)

profile = {'x':np.squeeze(grid),'y':profile}
uqresults[parameter] = UQResult('profile', data=getattr(fitresult,parameter), profiles=profile, threshold=threshold, noiselvl=noiselvl)
Expand Down
26 changes: 25 additions & 1 deletion deerlab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from types import FunctionType
from functools import wraps
import pickle
from tqdm.auto import tqdm
from joblib import Parallel

def parse_multidatasets(y_, A, weights, noiselvl, precondition=False, masks=None, subsets=None):
#===============================================================================
Expand Down Expand Up @@ -282,7 +284,6 @@ def goodness_of_fit(x,xfit,Ndof,noiselvl):
stats['aic'] - Akaike information criterion
stats['aicc'] - Corrected Akaike information criterion
stats['bic'] - Bayesian information criterion
stats['autocorr'] - Autocorrelation based on Durbin–Watson statistic
"""
sigma = noiselvl
Ndof = np.maximum(Ndof,1)
Expand Down Expand Up @@ -956,6 +957,29 @@ def choleskycovmat(Q,cholfactors):
return Σ
# ----------------------------------------------------------------------------------

class _ProgressParallel(Parallel):
"""
Patch for joblib.Parallel

Overrides the print_progress() method to enable the synchronous use of the TQDM bar
even for parallel processing.
"""
def __init__(self, use_tqdm=True, total=None, *args, **kwargs):
self._use_tqdm = use_tqdm
self._total = total
super().__init__(*args, **kwargs)

def __call__(self, *args, **kwargs):
with tqdm(disable=not self._use_tqdm, total=self._total) as self._pbar:
return Parallel.__call__(self, *args, **kwargs)

def print_progress(self):
if self._total is None:
self._pbar.total = self.n_dispatched_tasks
self._pbar.n = self.n_completed_tasks
self._pbar.refresh()

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

def _config():
import importlib
Expand Down
8 changes: 7 additions & 1 deletion examples/advanced/ex_profileanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
-------------------------------------------------------------------

How to obtain objective function profiles for the non-linear parameters of a model.

Profile analysis works by running the fitting routine multiple times while freezing the parameter of interest at different values.
This way, the profile of the objective function for this parameter can be obtained.
The confidence interval can then be estimated by comparing the profile to a statistical threshold.
To keep analysis time resonable, the number of samples for the profile can be specified via the ``samples`` keyword.
Additionally, the number of CPU cores for parallelization can be specified via the ``cores`` keyword.
"""

import numpy as np
Expand Down Expand Up @@ -43,7 +49,7 @@
print(results)

# Compute uncertainty with the likelihood profile method for the spin concentration and modulation depth parameters
profile_uq = dl.profile_analysis(Vmodel,Vexp,samples=20, parameters=['conc','mod'])
profile_uq = dl.profile_analysis(Vmodel,Vexp,samples=20, parameters=['conc','mod'], cores=4)

#%%
# Extract fitted dipolar signal
Expand Down
6 changes: 4 additions & 2 deletions examples/basic/ex_bootstrapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
confidence intervals to get accurate estimates of the uncertainty.
Conviniently, ``fit`` integrates bootstrapping to make it accessible via the keyword argument ``bootstrap`` which specifies the
number of samples to analyze to estimate the uncertainty. The larger this number, the more accurate
the confidence intervals but the longer the analysis will be. The standard for publication is typically 1000 samples.
the confidence intervals but the longer the analysis will be. The standard for publication is typically 250 samples.

To keep the analysis time reasonable, bootstrapping can be parallelized by specifying the number of CPU cores to use via the ``bootcores`` keyword.
"""

import numpy as np
Expand Down Expand Up @@ -48,7 +50,7 @@
Vmodel = dl.dipolarmodel(t,r, experiment = dl.ex_4pdeer(tau1,tau2, pathways=[1]))

# Fit the model to the data
results = dl.fit(Vmodel,Vexp,bootstrap=20)
results = dl.fit(Vmodel,Vexp,bootstrap=20,bootcores=4)

# In this example, just for the sake of time, we will just use 20 bootstrap samples.

Expand Down
19 changes: 19 additions & 0 deletions test/test_profile_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ def test_grids(model, mock_data):
assert np.allclose(x,grid['mean'])
# ======================================================================

# ======================================================================
def test_ncores(model, mock_data):
"Check that the number of cores can be specified"
from unittest.mock import patch
from deerlab.utils import _ProgressParallel

n_jobs_used = []

class TrackingParallel(_ProgressParallel):
def __init__(self, *args, **kwargs):
n_jobs_used.append(kwargs.get('n_jobs'))
super().__init__(*args, **kwargs)

with patch('deerlab.profile_analysis._ProgressParallel', TrackingParallel):
profile_analysis(model, mock_data, samples=5, noiselvl=noiselvl, cores=4)

assert any(n == 4 for n in n_jobs_used)
# ======================================================================

# ======================================================================
def test_docstring():
"Check that the docstring includes all variables and keywords."
Expand Down
Loading