From bc8e742d1bc6ad00ec6f7e03ef5cfc8fe8987d21 Mon Sep 17 00:00:00 2001 From: Hugo Karas Date: Mon, 13 Apr 2026 18:05:52 +0200 Subject: [PATCH 1/3] Fix issue with frozen Pmodels not being preserved by dipolarmodel.py --- deerlab/dipolarmodel.py | 4 +++- test/test_ddmodels.py | 15 ++++++++++++++- test/test_dipolarmodel.py | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/deerlab/dipolarmodel.py b/deerlab/dipolarmodel.py index 4371d5f9e..7076872e7 100644 --- a/deerlab/dipolarmodel.py +++ b/deerlab/dipolarmodel.py @@ -157,7 +157,9 @@ def _importparameter(parameter): 'par0' : parameter.par0, 'description' : parameter.description, 'unit' : parameter.unit, - 'linear' : parameter.linear + 'linear' : parameter.linear, + 'frozen' : parameter.frozen, + 'value' : parameter.value } #------------------------------------------------------------------------ diff --git a/test/test_ddmodels.py b/test/test_ddmodels.py index a3bfd125e..708439b0b 100644 --- a/test/test_ddmodels.py +++ b/test/test_ddmodels.py @@ -104,4 +104,17 @@ def test_dd_wormchain(): assert_ddmodel(dl.dd_wormchain) def test_dd_wormgauss(): - assert_ddmodel(dl.dd_wormgauss) \ No newline at end of file + assert_ddmodel(dl.dd_wormgauss) + + +def test_freezing_model(): + "Check that freezing parameters of a model works as expected" + + # Create model and freeze parameters + model = dl.dd_gauss + model.mean.freeze(3) + model.std.freeze(0.2) + + # Check that the frozen parameters are correctly set + assert model.mean.frozen and model.mean.value == 3 + assert model.std.frozen and model.std.value == 0.2 \ No newline at end of file diff --git a/test/test_dipolarmodel.py b/test/test_dipolarmodel.py index f7b987e03..7d655dd80 100644 --- a/test/test_dipolarmodel.py +++ b/test/test_dipolarmodel.py @@ -241,6 +241,21 @@ def test_fit_3pathways(V3path): assert np.allclose(result.model,V3path) # ====================================================================== +# ====================================================================== +def test_freeze_fit_linear(V1path): + "Check that the model can be correctly fitted with a frozen linear parameter" + + dd_model = dd_gauss + dd_model.std.freeze(0.25) + + assert dd_model.std.frozen and dd_model.std.value == 0.25 + Vmodel = dipolarmodel(t,r,dd_gauss,bg_hom3d,npathways=1) + assert Vmodel.std.frozen and Vmodel.std.value == 0.25 + + result = fit(Vmodel,V1path,ftol=1e-4) + + assert np.allclose(result.std,0.25) +# ====================================================================== # Fixtures # ---------------------------------------------------------------------- @fixture(scope='module') From de1bae7b0cd6923fde5c746909c74d058060049a Mon Sep 17 00:00:00 2001 From: Hugo Karas Date: Tue, 14 Apr 2026 07:32:08 +0200 Subject: [PATCH 2/3] Added copy to tests --- test/test_ddmodels.py | 2 +- test/test_model_penalty.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_ddmodels.py b/test/test_ddmodels.py index 708439b0b..dd05496e3 100644 --- a/test/test_ddmodels.py +++ b/test/test_ddmodels.py @@ -111,7 +111,7 @@ def test_freezing_model(): "Check that freezing parameters of a model works as expected" # Create model and freeze parameters - model = dl.dd_gauss + model = dl.dd_gauss.copy() model.mean.freeze(3) model.std.freeze(0.2) diff --git a/test/test_model_penalty.py b/test/test_model_penalty.py index 7919e89cc..380c14d61 100644 --- a/test/test_model_penalty.py +++ b/test/test_model_penalty.py @@ -66,6 +66,7 @@ def test_weight_freeze(penalty_fcn): penaltyobj = Penalty(penalty_fcn,'icc') penaltyobj.weight.freeze(0.5) assert penaltyobj.weight.frozen==True and penaltyobj.weight.value==0.5 + penaltyobj.weight.unfreeze() # ====================================================================== # ====================================================================== @@ -74,6 +75,7 @@ def test_fit(penalty_fcn, model, mock_data, selection): "Check fitting with a penalty with ICC-selected weight" penaltyobj = Penalty(penalty_fcn, selection) penaltyobj.weight.set(lb=1e-6,ub=1e1) + assert not penaltyobj.weight.frozen result = fit(model,mock_data,x,penalties=penaltyobj) assert ovl(result.model,mock_data)>0.975 # ====================================================================== @@ -89,6 +91,7 @@ def test_fit_with_penalty_weight(penalty_fcn, model, mock_data, case): penaltyobj.weight.freeze(0.00001) result = fit(model,mock_data,x,penalties=penaltyobj) assert ovl(result.model,mock_data)>0.975 + penaltyobj.weight.unfreeze() # ====================================================================== # ====================================================================== From 98c2a908aedcc824137926de2de5eed82d598ce8 Mon Sep 17 00:00:00 2001 From: Hugo Karas Date: Tue, 14 Apr 2026 07:51:18 +0200 Subject: [PATCH 3/3] Automatically copy the `bg_model`and `dd_model` upon import --- deerlab/__init__.py | 14 ++++++++++++-- deerlab/bg_models.py | 17 +++++++++++++++++ deerlab/dd_models.py | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/deerlab/__init__.py b/deerlab/__init__.py index 8a8d26e77..43cd8a84d 100644 --- a/deerlab/__init__.py +++ b/deerlab/__init__.py @@ -1,6 +1,16 @@ # __init__.py -from .dd_models import * -from .bg_models import * +from . import dd_models as _dd_models_mod +from . import bg_models as _bg_models_mod + +# Define __getattr__ early so submodules that do `from deerlab import bg_*` +# during their own import (e.g. dipolarmodel) can resolve names via this hook. +def __getattr__(name): + if name in _dd_models_mod.__all__: + return _dd_models_mod.__getattr__(name) + if name in _bg_models_mod.__all__: + return _bg_models_mod.__getattr__(name) + raise AttributeError(f"module 'deerlab' has no attribute {name!r}") + from .model import Model, Penalty, Parameter, link, lincombine, merge, relate from .deerload import deerload from .selregparam import selregparam diff --git a/deerlab/bg_models.py b/deerlab/bg_models.py index c0a11c8f4..0345b0a89 100644 --- a/deerlab/bg_models.py +++ b/deerlab/bg_models.py @@ -7,6 +7,7 @@ import math as m from numpy import pi import inspect +from copy import deepcopy as _deepcopy from deerlab.dipolarkernel import dipolarkernel from deerlab.utils import formatted_table from deerlab.model import Model @@ -513,3 +514,19 @@ def _poly3(t,p0,p1,p2,p3): bg_poly3.p3.set(description='3rd order weight', lb=-200, ub=200, par0=-1, unit=r'μs\ :sup:`-3`') # Add documentation bg_poly3.__doc__ = _docstring(bg_poly3,notes) + + +# --------------------------------------------------------------------------- +# Return a fresh deepcopy on every attribute access so that modifications +# to a retrieved model never affect the global template. +# --------------------------------------------------------------------------- +_templates = {name: obj for name, obj in list(globals().items()) if name.startswith('bg_')} +for _name in list(_templates): + del globals()[_name] + +__all__ = list(_templates.keys()) + +def __getattr__(name): + if name in _templates: + return _deepcopy(_templates[name]) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/deerlab/dd_models.py b/deerlab/dd_models.py index cab6ee244..2bca33785 100644 --- a/deerlab/dd_models.py +++ b/deerlab/dd_models.py @@ -7,6 +7,7 @@ import inspect import numpy as np import scipy.special as spc +from copy import deepcopy as _deepcopy from deerlab.model import Model from deerlab.utils import formatted_table @@ -1042,3 +1043,19 @@ def _wormgauss(r,contour,persistence,std): dd_wormgauss.std.set(description='Gaussian standard deviation', lb=0.01, ub=5, par0=0.2, unit='nm') # Add documentation dd_wormgauss.__doc__ = _dd_docstring(dd_wormgauss,notes) + docstr_example('dd_wormgauss') + + +# --------------------------------------------------------------------------- +# Return a fresh deepcopy on every attribute access so that modifications +# to a retrieved model never affect the global template. +# --------------------------------------------------------------------------- +_templates = {name: obj for name, obj in list(globals().items()) if name.startswith('dd_')} +for _name in list(_templates): + del globals()[_name] + +__all__ = list(_templates.keys()) + +def __getattr__(name): + if name in _templates: + return _deepcopy(_templates[name]) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}")