Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
0.16.4
- fix: correctly handle params states when swapping to new model (#51)
- fix: ensure identical results when clicking per file and compute button (#47, #51)
- setup: support latest python versions (#32, #46)
- setup: support latest matplotlib versions (#32, #46)
- fix: remember user's choice upon curve open (#44, #45)
Expand Down
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ If you have Python installed, you may also install via
:target: https://codecov.io/gh/AFM-analysis/PyJibe
.. |Docs Status| image:: https://readthedocs.org/projects/pyjibe/badge/?version=latest
:target: https://readthedocs.org/projects/pyjibe/builds/

Use of AI
---------

The model Claude Sonnet 4.6 was used to fix bugs such as #47.
2 changes: 2 additions & 0 deletions pyjibe/fd/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,8 @@ def on_fit_all(self):
try:
# preprocessing could fail for bad data
self.tab_preprocess.apply_preprocessing(fdist)
# update model key and ancillary parameters before fitting
self.tab_fit.fit_update_parameters(fdist)
# external fitting model could fail
self.tab_fit.fit_approach_retract(fdist, update_ui=False)
except BaseException as e:
Expand Down
19 changes: 15 additions & 4 deletions pyjibe/fd/tab_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,14 @@ def anc_update_parameters(self, fdist):
if (atab.item(row, 0).checkState() ==
QtCore.Qt.CheckState.Checked):
# update initial parameters
# (block signals to prevent recursive on_params_init
# while applying ancillary values to itab)
itab.blockSignals(True)
for rr in range(itab.rowCount()):
if itab.verticalHeaderItem(rr).text() == label:
if value_text != "nan":
itab.item(rr, 1).setText(value_text)
itab.blockSignals(False)
row += 1
atab.blockSignals(False)
else:
Expand Down Expand Up @@ -345,14 +349,21 @@ def fit_update_parameters(self, fdist):
# set the model
# - resets params_initial if model changed
# - important for computing ancillary parameters
prev_model_key = fdist.fit_properties.get("model_key")
fdist.fit_properties["model_key"] = model_key
if fdist.fit_properties.get("params_initial", False):
# (cannot coerce this into one line, because "params_initial"
# can be None.)
if prev_model_key != model_key:
# Clear the ancillary cache so anc_update_parameters gets fresh
# ancillaries for the new model (the cache may contain stale
# values from the previous model's fit)
fdist._anc_cache = None
if (fdist.fit_properties.get("params_initial", False)
and prev_model_key == model_key):
# set the parameters of the previous fit
params = fdist.fit_properties["params_initial"]
else:
# use the initial model parameters
# use the initial model parameters (also when model changed, to
# ensure itab is set up with the new model's parameter labels so
# that anc_update_parameters can correctly apply ancillary values)
params = self.fit_parameters()

# parameter table
Expand Down
71 changes: 71 additions & 0 deletions tests/test_fd_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,74 @@ def test_set_indentation_depth_manually_infdoublespinbox(qtbot):
qtbot.keyClicks(war.tab_fit.sp_range_1, text_entered)
assert war.tab_fit.sp_range_1.value() == resulting_value
main_window.close()


def test_fit_all_matches_single_fit(qtbot):
"""on_fit_all must produce the same TSV output as fitting one-by-one."""
files = make_directory_with_data(2)
tsv_path = files[0].parent / "pyjibe_fit_results_leaf.tsv"

main_window = pyjibe.head.PyJibe()
qtbot.addWidget(main_window)
main_window.load_data(files=files)
war = main_window.subwindows[0].widget()
war.cb_autosave.setChecked(True)
war._autosave_override = 1 # always overwrite, avoid dialog
war.tab_preprocess.set_preprocessing(["compute_tip_position"])
war.tab_fit.cb_weight_cp.setCheckState(QtCore.Qt.CheckState.Unchecked)

# Click through both files via the single-curve path
cl1 = war.list_curves.currentItem()
cl2 = war.list_curves.itemBelow(cl1)
war.list_curves.setCurrentItem(cl1)
war.list_curves.setCurrentItem(cl2)
assert tsv_path.exists(), "autosave TSV not created after single-fit"
single_tsv = tsv_path.read_text()

# Re-fit everything via fit-all; TSV must be byte-for-byte identical
war.on_fit_all()
fitall_tsv = tsv_path.read_text()

assert single_tsv == fitall_tsv
main_window.close()


def test_fit_all_matches_single_fit_kvm(qtbot):
"""fit-all must produce the same TSV as single-fit for the KVM model.

KVM has ancillary parameters (eta, time_ind) that are derived by fitting
a preliminary Hertz model to each curve. The KVM model is loaded via
PyJibe's extension system at startup.
"""
files = make_directory_with_data(2)
tsv_path = files[0].parent / "pyjibe_fit_results_leaf.tsv"

main_window = pyjibe.head.PyJibe()
qtbot.addWidget(main_window)
main_window.load_data(files=files)
war = main_window.subwindows[0].widget()
war.cb_autosave.setChecked(True)
war._autosave_override = 1 # always overwrite, avoid dialog
war.tab_preprocess.set_preprocessing(["compute_tip_position"])
war.tab_fit.cb_weight_cp.setCheckState(QtCore.Qt.CheckState.Unchecked)

# Switch to the KVM model (loaded via PyJibe's extension system)
idx = war.tab_fit.cb_model.findData("hertz_corr_visco_KVM")
if idx < 0:
pytest.skip("KVM model extension not installed in this environment")
war.tab_fit.cb_model.setCurrentIndex(idx)

# Click through both files via the single-curve path
cl1 = war.list_curves.currentItem()
cl2 = war.list_curves.itemBelow(cl1)
war.list_curves.setCurrentItem(cl1)
war.list_curves.setCurrentItem(cl2)
assert tsv_path.exists(), "autosave TSV not created after single-fit"
single_tsv = tsv_path.read_text()

# Re-fit everything via fit-all; TSV must be byte-for-byte identical
war.on_fit_all()
fitall_tsv = tsv_path.read_text()

assert single_tsv == fitall_tsv
main_window.close()
Loading