Skip to content

Commit

Permalink
Release v0.14.3 (#357)
Browse files Browse the repository at this point in the history
* docs: force light theme even in dark themed browsers (#350)

* fit: avoid unfreezing of all parameters when applying normalization (#352)

* Background models mathematical fixes (#351)

* bg_models: fix sign of background phase-shift's time-dependence

* add strong tests against numerical integration

* fix small mathematical errors in bg_homfractal models

* Fix scheduled CI test suite (#353)

* fix scheduled CI

- deprecate testing of Python 3.6 and 3.7
- add MKL linking to Windows
- fix pip installation errors

* fix typo in package name

* Modelling system quality of life improvements (#354)

* model: add method setas to import parameter metadata

* add hyperparameter info to results summary

* improve attribute request error messages in FitResult object

* improve error messaging during model evaluation

* bump VERSION to v0.14.3

* update CHANGELOG

* minor documentation update

* Fix errors in GHA publishing workflow (#355)

* fix build and publish to PyPI and Anaconda

- deprecate conda support for python 3.6 and 3.7

* minor edit

* Minor edits to files edited in previous PRs (#356)

* fix minor logic error

* minor documentation update

* fix minor bug
  • Loading branch information
luisfabib authored Jul 30, 2022
1 parent 384deb6 commit 11f4b1d
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 49 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci_PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install wheel
pip install .
- name: Windows MKL linking
Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/ci_scheduled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
python-version: [3.8, 3.9, "3.10", "3.11"]
steps:
- uses: actions/checkout@v2

Expand Down Expand Up @@ -52,7 +52,13 @@ jobs:
if: steps.cache.outputs.cache-hit != 'false'
run: |
python -m pip install --upgrade pip
python setup.py install
pip install wheel
pip install .
- name: Windows MKL linking
if: matrix.os == 'windows-latest'
run: |
python upgrade_mkl.py
- name: Test with pytest
run: pytest
2 changes: 1 addition & 1 deletion .github/workflows/docs_PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- '.github/workflows/examples_PR.yml'
jobs:

deploy:
docsbuild:
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/package_upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Build distribution
run: |
python setup.py sdist
./setup.py bdist_wheel
./setup.py bdist_wheel --user
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand Down Expand Up @@ -53,4 +53,4 @@ jobs:
subdir: 'conda.recipe'
anacondatoken: ${{ secrets.ANACONDA_TOKEN }}
platforms: 'osx-64 linux-32 linux-64 win-32 win-64'
python: '3.6 3.7 3.8 3.9 3.10'
python: '3.8 3.9 3.10'
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.14.2
v0.14.3
10 changes: 5 additions & 5 deletions deerlab/bg_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def _hom3dphase(t,conc,lam):
conc = conc*1e-6*1e3*Nav # umol/L -> mol/L -> mol/m^3 -> spins/m^3
# Compute background function
ξ = 8*pi/9/np.sqrt(3)*(np.sqrt(3)+np.log(2-np.sqrt(3)))*D
B = np.exp(-1j*ξ*lam*conc*(t*1e-6))
B = np.exp(1j*ξ*lam*conc*(t*1e-6))

return B
# Create model
Expand Down Expand Up @@ -246,7 +246,7 @@ def _hom3dex_phase(t,conc,rex,lam):

# Background function
C_k = - Ic - np.squeeze(Vex*(dipolarkernel(t,rex,integralop=False,complex=True)).imag)
B = np.exp(-1j*lam*conc*C_k)
B = np.exp(1j*lam*conc*C_k)

return B
# Create model
Expand Down Expand Up @@ -284,7 +284,7 @@ def _homfractal(t,fconc,fdim,lam):
elif d==4.5:
κd = 5.35506 # d->4.5 limit of general expression
else:
κd = 2/9*(-1)**(-d/3)*pi*np.cos(d*pi/6)*gamma(-d/3)*(
κd = 2/9*(-1)**(-d/3+1)*pi*np.cos(d*pi/6)*gamma(-d/3)*(
(-1 + (-1)**(d/3))*np.sqrt(3*np.pi)*gamma(1+d/3)/gamma(3/2+d/3)
+ 6*hyp2f1_repro(1/2, -d/3, 3/2, 3)
)
Expand Down Expand Up @@ -330,13 +330,13 @@ def _homfractal_phase(t,fconc,fdim,lam):
elif d==4.5:
ξd = 1j*np.inf # Limit of d->4.5 of equation below
else:
ξd = D**(d/3)*pi**(3/2)/9/gamma(3/2 + d/3) * (
ξd = 2*D**(d/3)*pi**(3/2)/9/gamma(3/2 + d/3) * (
np.sqrt(3)*pi*np.cos(d*pi/6)/np.cos(d*pi/3)
- 2**(2+2*d/3)*3**(1 + d/3)*gamma(-1-2*d/3)*np.sin(d*pi/6)*gamma(3/2+d/3)*hyp2f1((-3-2*d)/6, -d/3, (3-2*d)/6, 1/3)/gamma((3-2*d)/6)
)

# Compute background function
B = np.exp(-1j*ξd*fconc*lam*np.sign(t)*abs(t*1e-6)**(d/3))
B = np.exp(1j*ξd*fconc*lam*np.sign(t)*abs(t*1e-6)**(d/3))
return B
# ======================================================================
# Create model
Expand Down
14 changes: 10 additions & 4 deletions deerlab/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from scipy.linalg import block_diag
from scipy.optimize import brentq
from scipy.interpolate import interp1d
import difflib

class FitResult(dict):
# ========================================================================
Expand Down Expand Up @@ -48,12 +49,17 @@ class FitResult(dict):
using the `keys()` method.
"""

def __getattr__(self, name):
def __getattr__(self, attr):
try:
return self[name]
return self[attr]
except KeyError:
raise AttributeError(name)

errstr = f"The results object has no attribute '{attr}'."
attributes = [key for key in self.keys()]
proposal = difflib.get_close_matches(attr, attributes)
if len(proposal)>0:
errstr += f' \n Did you mean: {proposal} ?'
raise AttributeError(errstr)

__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__

Expand Down
86 changes: 73 additions & 13 deletions deerlab/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ def set(self,**attributes):
return
#---------------------------------------------------------------------------------------

#---------------------------------------------------------------------------------------
def setas(self,parameter):
"""
Copy all attributes from an input parameter to the current parameter.
Parameters
----------
param : ``Parameter`` object
Model parameters from which to extract the attributes.
"""
self.set(**_importparameter(parameter))
#---------------------------------------------------------------------------------------

#---------------------------------------------------------------------------------------
def freeze(self,value):
"""
Expand Down Expand Up @@ -266,7 +279,7 @@ def __getattribute__(self, attr):
try:
return super(Model, self).__getattribute__(attr)
except AttributeError:
errstr = f'The model has no attribute {attr}.'
errstr = f"The model has no attribute '{attr}'."
attributes = [key for key in self.__dict__]
proposal = difflib.get_close_matches(attr, attributes)
if len(proposal)>0:
Expand Down Expand Up @@ -506,19 +519,38 @@ def __call__(self,*args,**kargs):
or keyword arguments and evaluates the model.
"""
# Check that the correct number of arguments have been specified
Nrequired = len(self._parameter_list())
Nrequired += len(self._constantsInfo)
paramlist = self._parameter_list(order='vector')
constlist = [info['argkey'] for info in self._constantsInfo]
Nrequired = len(paramlist)
Nrequired += len(constlist)
if (len(args)+len(kargs))!=Nrequired:
raise SyntaxError(f'The model requires {Nrequired} arguments, but {len(args)+len(kargs)} have been specified.')

# Positional arguments
args_constants= [np.atleast_1d(args[info['argidx']]) for info in self._constantsInfo if info['argidx']<len(args)]
args_list = [np.atleast_1d(arg) for idx,arg in enumerate(args) if idx not in [info['argidx'] for info in self._constantsInfo]]
# Extract positional arguments
args_constants= [np.atleast_1d(args[info['argidx']]) for info in self._constantsInfo if info['argidx']<len(args)]
args_list = [np.atleast_1d(arg) for idx,arg in enumerate(args) if idx not in [info['argidx'] for info in self._constantsInfo]]

# Check keyword arguments
paramlist_ = paramlist[len(args_list):]
karg_keys = kargs.keys()
for key in karg_keys:
if key in paramlist and key not in paramlist_:
raise SyntaxError(f'The parameter "{key}" has been specified twice.')
if key not in paramlist and not key in constlist:
errstr = f'The argument \"{key}\" is not part of the model signature.'
proposal = difflib.get_close_matches(key, paramlist)
print(paramlist,proposal)
if len(proposal)>0:
errstr += f' \n\t\tDid you mean: {proposal}?'
raise AttributeError(errstr)
for param in paramlist_:
if param not in karg_keys:
raise KeyError(f'The parameter "{param}" has not been specified.')

# Extract keywords arguments
kargs_constants = [np.atleast_1d(kargs[info['argkey']]) for info in self._constantsInfo if info['argkey'] in kargs]
kargs_list = [np.atleast_1d(kargs[param]) for param in paramlist_]

# Keywords arguments
kargs_constants = [np.atleast_1d(kargs[info['argkey']]) for info in self._constantsInfo if info['argkey'] in kargs]
kargs_list = [np.atleast_1d(kargs[param]) for param in self._parameter_list(order='vector')[len(args_list):]]

constants = args_constants + kargs_constants
param_list = args_list + kargs_list

Expand Down Expand Up @@ -904,6 +936,29 @@ def _print_fitresults(fitresult,model):
string += 'Goodness-of-fit: \n'
string += formatted_table(table,alignment) + '\n'


hasregularization = fitresult.regparam!=0
haspenalties = fitresult.penweights
if hasregularization or haspenalties:
string += 'Model hyperparameters: \n'
tags = []
values = []
alignment = []
if hasregularization:
alignment.append('^')
tags.append('Regularization parameter')
regparam = fitresult.regparam
if regparam is None: regparam = 0
values.append(regparam)
if haspenalties:
for n,penweight in enumerate(fitresult.penweights):
alignment.append('^')
tags.append(f'Penalty weight #{n+1}')
values.append(penweight)
values = [f'{var:.3f}' if var<1e4 else f'{var:.3g}' for var in values]
table = [tags,values]
string += formatted_table(table,alignment) + '\n'

# Construct table of model parameters fits
table = []
table.append([f'Parameter','Value','95%-Confidence interval','Unit','Description']) # Header
Expand Down Expand Up @@ -1048,12 +1103,15 @@ def fit(model_, y, *constants, par0=None, penalties=None, bootstrap=0, noiselvl=
model.addlinear('scale',lb=-np.inf,ub=np.inf,description='Scaling factor')

normalization = False
normfactor_keys = []
for key in model._parameter_list():
param = getattr(model,key)
if np.all(param.linear):
if param.normalization is not None:
model.addnonlinear(f'{key}_scale',lb=-np.inf,ub=np.inf,par0=1,description=f'Normalization factor of {key}')
getattr(model,f'{key}_scale').freeze(1)
normfactor_key = f'{key}_scale'
normfactor_keys.append(normfactor_key)
model.addnonlinear(normfactor_key,lb=-np.inf,ub=np.inf,par0=1,description=f'Normalization factor of {key}')
getattr(model,normfactor_key).freeze(1)
normalization = True

# Get boundaries and conditions for the linear and nonlinear parameters
Expand Down Expand Up @@ -1167,12 +1225,14 @@ def bootstrap_fcn(ysim):
if normalization:
for key in keys:
param = getattr(model,key)
param.unfreeze()
if key in normfactor_keys:
param.unfreeze()
if np.all(param.linear):
if param.normalization is not None:
non_normalized = FitResult_param_[key] # Non-normalized value
FitResult_param_[key] = param.normalization(FitResult_param_[key]) # Normalized value
FitResult_param_[f'{key}_scale'] = np.mean(non_normalized/FitResult_param_[key]) # Normalization factor
FitResult_param_[f'{key}_scale'] = np.mean(non_normalized/FitResult_param_[key]) # Normalization factor
FitResult_paramuq_[f'{key}Uncert'] = FitResult_paramuq_[f'{key}Uncert'].propagate(lambda x: x/FitResult_param_[f'{key}_scale'], lb=param.lb, ub=param.ub) # Normalization of the uncertainty
FitResult_paramuq_[f'{key}_scaleUncert'] = UQResult('void')
if len(noiselvl)==1:
Expand Down
18 changes: 18 additions & 0 deletions docsrc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ Release Notes
- |fix| : Something which was not working as expected or leading to errors has been fixed.
- |api| : This will require changes in your scripts or code.


Release v0.14.3 - July 2022
---------------------------------

- |api| Deprecated support for Python 3.6 and 3.7 (:pr:`353`).
- |feature| Added multiple quality of life improvements to the modelling system (:pr:`354`).

* Add new method ``paramA.setas(paramB)`` for ``Parameter`` objects to copy the full metadata content from ``paramB`` into ``paramA``.
* Expand the ``FitResult`` summary to report on the regularization parameter and penalty weights when used in the analysis.
* Improve the report of incorrect attribute requests in ``FitResult`` objects and provide close matches as suggestions.
* Improve the report of errors during the evaluation of ``Model`` objects.

- |fix| Fix bug in the ``fit`` function unfreezing all frozen model parameters upon fitting if any model parameter included a normalization constraint (:issue:`348`, :pr:`352`).
- |fix| Corrected two minor mathematical errors in the physical background models ``bg_homfractal`` and ``bg_homfractal_phase`` (:pr:`351`).
- |fix| Fixed display of the online documentation in browsers with an enabled dark theme that made certain menus and text sections unreadable (:issue:`349`, :pr:`350`). The documentation will now default to a light theme even for dark themed browser.



Release v0.14.2 - June 2022
---------------------------------

Expand Down
3 changes: 2 additions & 1 deletion docsrc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@
"github_repo": "DeerLab", # Repo name
"github_version": "master", # Version
"conf_py_path": "/source/", # Path in the checkout to the docs root
'version' : version,
'version' : version,
"default_mode": "light",
}
html_theme_options = {
"navbar_start": ["navbar-logo"],
Expand Down
2 changes: 1 addition & 1 deletion docsrc/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Requirements

To install DeerLab, first install Python on your computer. Python can be downloaded from the `official Python distribution <https://www.python.org/>`_. There are
many online tutorials to guide you through the installation and setup (see `here <https://realpython.com/installing-python/>`_ for example). Make sure you install
one of the Python versions compatible with DeerLab, either **Python 3.6**, **3.7**, **3.8**, **3.9**, or **3.10**.
one of the Python versions compatible with DeerLab, either **Python 3.8**, **3.9**, or **3.10**.

.. rubric:: Windows systems

Expand Down
54 changes: 36 additions & 18 deletions test/test_bgmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,38 +62,56 @@ def test_bg_hom3d_value():
conc = 143
lam = 0.47
B = dl.bg_hom3d(t, conc, lam)
Bref = 0.9227092
assert abs(B - Bref) < 1e-7
Bref = 0.92269778443975 # Value from numerical integration
assert abs(B - Bref) < 1e-4


def test_bg_hom3d_phase():
Bref = 0.9998641918107823 - 0.01648022859583373j
assert_bgmodel_value(dl.bg_hom3d_phase, Bref)

def test_bg_hom3d_phase_value():
t = 1.2
conc = 143
lam = 0.47
B = dl.bg_hom3d_phase(t, conc, lam)
Bref = 0.99994353+0.01062727j # Value from numerical integration
assert np.abs(B - Bref) < 1e-4

def test_bg_homfractal3d_value():
conc = 100
dim = 3
lam = 0.423
def test_bg_homfractal_value():
conc = 1e-3
dim = 2.2
lam = 0.47
t = 0.1
B = dl.bg_homfractal(t, conc, dim, lam)
Bref = 0.99578995
assert abs(B - Bref) < 1e-6
Bref = 0.9443678720378192 # Value from numerical integration
assert abs(B - Bref) < 1e-4


def test_bg_homfractal_phase():
Bref = 0.999999988356966 - 0.00015259773164288322j
assert_bgmodel_value(dl.bg_homfractal_phase, Bref)
conc = 1e-3
dim = 2.2
lam = 0.47
t = 0.1
B = dl.bg_homfractal_phase(t, conc, dim, lam)
Bref = 0.9999771819749+0.006755407429425621j # Value from numerical integration
assert abs(B - Bref) < 1e-4


def test_bg_hom3dex():
Bref = 0.8828975362813715
assert_bgmodel_value(dl.bg_hom3dex, Bref)
t = 1.2
Rex = 3
conc = 143
lam = 0.47
B = dl.bg_hom3dex(t, conc, Rex, lam)
Bref = 0.926971275363862 # Value from numerical integration
assert abs(B - Bref) < 1e-4


def test_bg_hom3dex_phase():
Bref = 0.9998641899814665 - 0.016480339581022234j
assert_bgmodel_value(dl.bg_hom3dex_phase, Bref)
t = 1.2
Rex = 3
conc = 143
lam = 0.47
B = dl.bg_hom3dex_phase(t, conc, Rex, lam)
Bref = 0.999943779807736+0.010603642007255394j # Value from numerical integration
assert abs(B - Bref) < 1e-4


def test_bg_exp_value():
Expand Down
Loading

0 comments on commit 11f4b1d

Please sign in to comment.