From dc143d997556057d58ff16722f8e67db205f17aa Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Tue, 10 Feb 2026 14:09:14 +0100 Subject: [PATCH 1/6] fix: cache the compute ancilary params at indentation level --- CHANGELOG | 2 ++ src/nanite/indent.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index af31b26..4d058c2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +4.2.3 + - fix: cache ancillary params per Indentation curve (#27) 4.2.2 - ref: rewrite asserts to ValueErrors 4.2.1 diff --git a/src/nanite/indent.py b/src/nanite/indent.py index f8cceb1..c43cc7b 100644 --- a/src/nanite/indent.py +++ b/src/nanite/indent.py @@ -32,6 +32,10 @@ def __init__(self, data, metadata, diskcache=None): # Curve rating (see `self.rate_quality`) self._rating = None + # handle ancillary param caching + self._anc_cache = {} + self._anc_valid = False + @property def data(self): warnings.warn("Please use __getitem__ instead!", DeprecationWarning) @@ -258,15 +262,29 @@ def fit_model(self, **kwargs): self["fit range"] = fitter.fit_range self.fit_properties = fitter.fp + # handle ancill caching + self._anc_valid = False + def get_ancillary_parameters(self, model_key=None): """Compute ancillary parameters for the current model""" + # handle ancill caching + if self._anc_valid: + return self._anc_cache + if model_key is None: if "model_key" in self.fit_properties: model_key = self.fit_properties["model_key"] else: model_key = FP_DEFAULT["model_key"] - return model.compute_anc_parms(idnt=self, - model_key=model_key) + + anc = model.compute_anc_parms(idnt=self, + model_key=model_key) + # handle ancill caching + if self.fit_properties.get("success", False): + self._anc_cache = anc + self._anc_valid = True + + return anc def get_initial_fit_parameters(self, model_key=None, common_ancillaries=True, From 52fdeb7ee003ab98359040d56b192426bd1bf261 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Wed, 11 Feb 2026 17:02:20 +0100 Subject: [PATCH 2/6] tests: create ancill request test; enh: remove stale cache prior to new fit --- src/nanite/indent.py | 14 ++++----- tests/test_fit_ancillary.py | 61 +++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/nanite/indent.py b/src/nanite/indent.py index c43cc7b..d03de36 100644 --- a/src/nanite/indent.py +++ b/src/nanite/indent.py @@ -32,8 +32,8 @@ def __init__(self, data, metadata, diskcache=None): # Curve rating (see `self.rate_quality`) self._rating = None - # handle ancillary param caching - self._anc_cache = {} + # ancillary param caching + self._anc_cache = None self._anc_valid = False @property @@ -252,6 +252,10 @@ def fit_model(self, **kwargs): # properties are the same. pass else: + # invalidate the cache + self._anc_valid = False + self._anc_cache = None + fitter = IndentationFitter(self) # Perform fitting # Note: if `fitter.fp["success"]` is `False`, then @@ -262,12 +266,8 @@ def fit_model(self, **kwargs): self["fit range"] = fitter.fit_range self.fit_properties = fitter.fp - # handle ancill caching - self._anc_valid = False - def get_ancillary_parameters(self, model_key=None): """Compute ancillary parameters for the current model""" - # handle ancill caching if self._anc_valid: return self._anc_cache @@ -279,7 +279,7 @@ def get_ancillary_parameters(self, model_key=None): anc = model.compute_anc_parms(idnt=self, model_key=model_key) - # handle ancill caching + # handle ancill cache if self.fit_properties.get("success", False): self._anc_cache = anc self._anc_valid = True diff --git a/tests/test_fit_ancillary.py b/tests/test_fit_ancillary.py index a612478..ce62959 100644 --- a/tests/test_fit_ancillary.py +++ b/tests/test_fit_ancillary.py @@ -8,7 +8,6 @@ from common import MockModelModule - data_path = pathlib.Path(__file__).parent / "data" jpkfile = data_path / "fmt-jpk-fd_spot3-0192.jpk-force" @@ -22,10 +21,10 @@ def compute_ancillaries(*args, **kwargs): raise ValueError("Not computed") with MockModelModule( - compute_ancillaries=compute_ancillaries, - parameter_anc_keys=["J"], - parameter_anc_names=["ancillary J guess"], - parameter_anc_units=["Pa"], + compute_ancillaries=compute_ancillaries, + parameter_anc_keys=["J"], + parameter_anc_names=["ancillary J guess"], + parameter_anc_units=["Pa"], model_key="test2"): # We need to perform preprocessing first, if we want to get the # correct initial contact point. @@ -45,10 +44,10 @@ def test_simple_ancillary_override(): idnt = ds1[0] with MockModelModule( - compute_ancillaries=lambda x: {"E": 1580}, - parameter_anc_keys=["E"], - parameter_anc_names=["ancillary E guess"], - parameter_anc_units=["Pa"], + compute_ancillaries=lambda x: {"E": 1580}, + parameter_anc_keys=["E"], + parameter_anc_names=["ancillary E guess"], + parameter_anc_units=["Pa"], model_key="test1"): # We need to perform preprocessing first, if we want to get the # correct initial contact point. @@ -71,10 +70,10 @@ def test_simple_ancillary_override_nan(): idnt = ds1[0] with MockModelModule( - compute_ancillaries=lambda x: {"E": np.nan}, - parameter_anc_keys=["E"], - parameter_anc_names=["ancillary E guess"], - parameter_anc_units=["Pa"], + compute_ancillaries=lambda x: {"E": np.nan}, + parameter_anc_keys=["E"], + parameter_anc_names=["ancillary E guess"], + parameter_anc_units=["Pa"], model_key="test2"): # We need to perform preprocessing first, if we want to get the # correct initial contact point. @@ -89,3 +88,39 @@ def test_simple_ancillary_override_nan(): 1584.8876592662375, atol=1, rtol=0) + + +def test_request_ancillary_parameters(): + """request the ancillary parameters after fitting""" + ds1 = nanite.IndentationGroup(jpkfile) + idnt = ds1[0] + + model_key = "test4" + with MockModelModule(model_key=model_key): + # We need to perform preprocessing first, if we want to get the + # correct initial contact point. + idnt.apply_preprocessing(["compute_tip_position"]) + # We set the baseline fixed, because this test was written so) + params_initial = idnt.get_initial_fit_parameters(model_key=model_key) + params_initial["baseline"].set(vary=False) + idnt.fit_model(model_key=model_key, + params_initial=params_initial) + + # ancillary parameters are not yet requested + assert idnt._anc_cache is None + assert not idnt._anc_valid + + # actually request the ancillary parameters, as done in pyjibe + anc = idnt.get_ancillary_parameters( + model_key=model_key) + + # check ancillaries + assert idnt._anc_cache is not None + assert idnt._anc_valid + assert np.allclose(anc["max_indent"], 3.669487775650337e-07, + atol=1e-10, rtol=0) + + # check params_initial and params_fitted + assert idnt.fit_properties["params_initial"]["E"].value == 3000 + assert np.allclose(idnt.fit_properties["params_fitted"]["E"].value, + 1584.8876592662375, atol=1, rtol=0) From 0415ca23edb5c93ec4aad2b28629bd51a1426a32 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Thu, 12 Feb 2026 10:35:46 +0100 Subject: [PATCH 3/6] tests: add new ancillary to caching test --- tests/test_fit_ancillary.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_fit_ancillary.py b/tests/test_fit_ancillary.py index ce62959..22553f9 100644 --- a/tests/test_fit_ancillary.py +++ b/tests/test_fit_ancillary.py @@ -96,7 +96,12 @@ def test_request_ancillary_parameters(): idnt = ds1[0] model_key = "test4" - with MockModelModule(model_key=model_key): + with MockModelModule( + compute_ancillaries=lambda x: {"amazing_ancillary": 42.314}, + parameter_anc_keys=["amazing_ancillary"], + parameter_anc_names=["Amazing Ancillary"], + parameter_anc_units=["m/s"], + model_key=model_key): # We need to perform preprocessing first, if we want to get the # correct initial contact point. idnt.apply_preprocessing(["compute_tip_position"]) @@ -117,8 +122,12 @@ def test_request_ancillary_parameters(): # check ancillaries assert idnt._anc_cache is not None assert idnt._anc_valid + # max_indent is a common ancillary assert np.allclose(anc["max_indent"], 3.669487775650337e-07, atol=1e-10, rtol=0) + # new ancillary for this model + assert np.allclose(anc["amazing_ancillary"], 42.314, + atol=1, rtol=0) # check params_initial and params_fitted assert idnt.fit_properties["params_initial"]["E"].value == 3000 From 7ab1bb7e3806b50c3d0003908624a7e4b0e510d3 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Thu, 12 Feb 2026 10:37:28 +0100 Subject: [PATCH 4/6] tests: show that computed ancill is equal to the cache --- tests/test_fit_ancillary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_fit_ancillary.py b/tests/test_fit_ancillary.py index 22553f9..cf89410 100644 --- a/tests/test_fit_ancillary.py +++ b/tests/test_fit_ancillary.py @@ -108,6 +108,7 @@ def test_request_ancillary_parameters(): # We set the baseline fixed, because this test was written so) params_initial = idnt.get_initial_fit_parameters(model_key=model_key) params_initial["baseline"].set(vary=False) + # the old ancillary cache is invalidated just before fitting idnt.fit_model(model_key=model_key, params_initial=params_initial) @@ -122,6 +123,7 @@ def test_request_ancillary_parameters(): # check ancillaries assert idnt._anc_cache is not None assert idnt._anc_valid + assert idnt._anc_cache == anc # max_indent is a common ancillary assert np.allclose(anc["max_indent"], 3.669487775650337e-07, atol=1e-10, rtol=0) From 465c8a1a08407689f50b088247495831298c9dcf Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Mon, 23 Feb 2026 11:13:21 +0100 Subject: [PATCH 5/6] ref: remove todo note for ancillary caching --- src/nanite/model/core.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/nanite/model/core.py b/src/nanite/model/core.py index fdb3d58..8c786fc 100644 --- a/src/nanite/model/core.py +++ b/src/nanite/model/core.py @@ -205,12 +205,8 @@ def compute_ancillaries(self, fd): ------- ancillaries: collections.OrderedDict key-value dictionary of ancillary parameters + """ - # TODO: - # - ancillaries are not cached yet (some ancillaries might depend on - # fitting interval or other initial parameters - take that into - # account) - # - "max_indent" actually belongs to "common_ancillaries" (see fit.py) anc_ord = OrderedDict() # general for key in ANCILLARY_COMMON: From b702f1687b5ee17593411d14acf08e17334f4e97 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Tue, 10 Mar 2026 11:51:22 +0100 Subject: [PATCH 6/6] ref: remove redundant _anc variable --- src/nanite/indent.py | 5 +---- tests/test_fit_ancillary.py | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/nanite/indent.py b/src/nanite/indent.py index d03de36..1892fc2 100644 --- a/src/nanite/indent.py +++ b/src/nanite/indent.py @@ -34,7 +34,6 @@ def __init__(self, data, metadata, diskcache=None): # ancillary param caching self._anc_cache = None - self._anc_valid = False @property def data(self): @@ -253,7 +252,6 @@ def fit_model(self, **kwargs): pass else: # invalidate the cache - self._anc_valid = False self._anc_cache = None fitter = IndentationFitter(self) @@ -268,7 +266,7 @@ def fit_model(self, **kwargs): def get_ancillary_parameters(self, model_key=None): """Compute ancillary parameters for the current model""" - if self._anc_valid: + if self._anc_cache: return self._anc_cache if model_key is None: @@ -282,7 +280,6 @@ def get_ancillary_parameters(self, model_key=None): # handle ancill cache if self.fit_properties.get("success", False): self._anc_cache = anc - self._anc_valid = True return anc diff --git a/tests/test_fit_ancillary.py b/tests/test_fit_ancillary.py index cf89410..197c4d5 100644 --- a/tests/test_fit_ancillary.py +++ b/tests/test_fit_ancillary.py @@ -114,7 +114,6 @@ def test_request_ancillary_parameters(): # ancillary parameters are not yet requested assert idnt._anc_cache is None - assert not idnt._anc_valid # actually request the ancillary parameters, as done in pyjibe anc = idnt.get_ancillary_parameters( @@ -122,7 +121,6 @@ def test_request_ancillary_parameters(): # check ancillaries assert idnt._anc_cache is not None - assert idnt._anc_valid assert idnt._anc_cache == anc # max_indent is a common ancillary assert np.allclose(anc["max_indent"], 3.669487775650337e-07,