pycat-napari 1.5.331__tar.gz → 1.5.332__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/CHANGELOG.md +36 -0
  2. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/PKG-INFO +1 -1
  3. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/pyproject.toml +1 -1
  4. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/condensate_physics_tools.py +98 -4
  5. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/vpt_tools.py +25 -27
  6. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/vpt_ui.py +71 -2
  7. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/.gitignore +0 -0
  8. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/CONTRIBUTING.md +0 -0
  9. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/LICENSE +0 -0
  10. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/README.md +0 -0
  11. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/THIRD_PARTY_LICENSES.txt +0 -0
  12. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/pycat-devbio-napari-env-arm-mac.yaml +0 -0
  13. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/pycat-devbio-napari-env-x86-windows.yaml +0 -0
  14. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/pycat-napari-env-arm-mac.yaml +0 -0
  15. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/pycat-napari-env-x86-windows.yaml +0 -0
  16. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/requirements-arm-mac.txt +0 -0
  17. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/requirements-base.txt +0 -0
  18. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/requirements-devbio-napari-arm-mac.txt +0 -0
  19. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/config/requirements-devbio-napari.txt +0 -0
  20. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/Makefile +0 -0
  21. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/audits/DEV_NOTES.md +0 -0
  22. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/audits/PyCAT_method_audit_2026-07-09.md +0 -0
  23. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/audits/PyCAT_napari_integration_audit_2026-07-09.md +0 -0
  24. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/logos/pycat_logo-2.png +0 -0
  25. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/logos/pycat_logo.png +0 -0
  26. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/logos/pycat_logo_1024.png +0 -0
  27. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/logos/pycat_logo_256.png +0 -0
  28. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/logos/pycat_logo_512.png +0 -0
  29. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/make.bat +0 -0
  30. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/requirements.txt +0 -0
  31. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/dark_mode.css +0 -0
  32. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/examples/Image 1 dapi.tif +0 -0
  33. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/examples/Image 1.tif +0 -0
  34. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/pycat_logo_512.png +0 -0
  35. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/Viewer_and_menu_highlights.png +0 -0
  36. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/cell_analyzer.png +0 -0
  37. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/condensate_segmentation.png +0 -0
  38. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/measuring_lines.png +0 -0
  39. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/opened_image_in_viewer.png +0 -0
  40. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/preprocessed_images.png +0 -0
  41. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_static/screenshots/save_and_clear_popup.png +0 -0
  42. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/_templates/autosummary/module.rst +0 -0
  43. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/index.rst +0 -0
  44. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.central_manager.rst +0 -0
  45. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.data.rst +0 -0
  46. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.file_io.rst +0 -0
  47. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.run_pycat.rst +0 -0
  48. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.toolbox.rst +0 -0
  49. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.ui.rst +0 -0
  50. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/api/pycat.utils.rst +0 -0
  51. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/conf.py +0 -0
  52. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/development/contributing.rst +0 -0
  53. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/development/index.rst +0 -0
  54. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/development/roadmap.rst +0 -0
  55. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/development/support.rst +0 -0
  56. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/features.rst +0 -0
  57. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/index.rst +0 -0
  58. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/installation.rst +0 -0
  59. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/usage/index.rst +0 -0
  60. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/source/usage/tutorials/nuclear_condensate_analysis_gui_tutorial.rst +0 -0
  61. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/docs/test.txt +0 -0
  62. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/notebooks/Synthtic Data Generation NB.ipynb +0 -0
  63. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/notebooks/int_truncated_dfs.ipynb +0 -0
  64. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/notebooks/pycat_plotting.ipynb +0 -0
  65. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/__init__.py +0 -0
  66. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/batch_processor.py +0 -0
  67. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/batch_step_registry.py +0 -0
  68. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/central_manager.py +0 -0
  69. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/data/__init__.py +0 -0
  70. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/data/data_modules.py +0 -0
  71. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/file_io/__init__.py +0 -0
  72. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/file_io/file_io.py +0 -0
  73. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/file_io/frap_io.py +0 -0
  74. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/file_io/metadata_extract.py +0 -0
  75. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/file_io/multidim_io.py +0 -0
  76. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/file_io/session_loader.py +0 -0
  77. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/fix_tifffile.py +0 -0
  78. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/icons/pycat_logo-2.png +0 -0
  79. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/icons/pycat_logo.png +0 -0
  80. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/icons/pycat_logo_1024.png +0 -0
  81. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/icons/pycat_logo_256.png +0 -0
  82. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/icons/pycat_logo_512.png +0 -0
  83. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/run_pycat.py +0 -0
  84. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/__init__.py +0 -0
  85. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/advanced_analysis_ui.py +0 -0
  86. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/analysis_plots.py +0 -0
  87. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/batch_roi_tools.py +0 -0
  88. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/benchmark_tools.py +0 -0
  89. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/brightfield_tools.py +0 -0
  90. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/brightfield_ui.py +0 -0
  91. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/clean_spot_detection_tools.py +0 -0
  92. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/condensate_physics_ui.py +0 -0
  93. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/contrast_cascade_tools.py +0 -0
  94. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/contrast_cascade_ui.py +0 -0
  95. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/correlation_func_analysis_tools.py +0 -0
  96. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/data_qc_tools.py +0 -0
  97. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/data_qc_ui.py +0 -0
  98. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/data_viz_tools.py +0 -0
  99. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/dynamic_spatial_tools.py +0 -0
  100. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fd_curve_tools.py +0 -0
  101. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fd_curve_ui.py +0 -0
  102. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/feature_analysis_tools.py +0 -0
  103. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fft_bandpass_tools.py +0 -0
  104. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fibril_tools.py +0 -0
  105. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fibril_ui.py +0 -0
  106. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/frap_tools.py +0 -0
  107. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/frap_ui.py +0 -0
  108. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fusion_tools.py +0 -0
  109. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/fusion_ui.py +0 -0
  110. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/gaussian_localization_tools.py +0 -0
  111. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/gpu_utils.py +0 -0
  112. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/image_processing_tools.py +0 -0
  113. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/intensity_profile_tools.py +0 -0
  114. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/invitro_bf_ui.py +0 -0
  115. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/invitro_fluor_ui.py +0 -0
  116. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/invitro_tools.py +0 -0
  117. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/label_and_mask_tools.py +0 -0
  118. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/layer_tools.py +0 -0
  119. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/molecular_counting_tools.py +0 -0
  120. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/morphological_complexity_tools.py +0 -0
  121. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/nb_tools.py +0 -0
  122. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/nb_ui.py +0 -0
  123. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/numba_utils.py +0 -0
  124. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/obj_based_coloc_analysis_tools.py +0 -0
  125. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/organizational_metrics_tools.py +0 -0
  126. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/partition_enrichment_tools.py +0 -0
  127. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/pipeline_diagnostic_tools.py +0 -0
  128. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/pipeline_snr_tools.py +0 -0
  129. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/pixel_wise_corr_analysis_tools.py +0 -0
  130. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/segmentation_tools.py +0 -0
  131. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/spatial_acf_tools.py +0 -0
  132. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/spatial_metrology_tools.py +0 -0
  133. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/spatial_metrology_ui.py +0 -0
  134. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/spatial_randomness_tools.py +0 -0
  135. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/spida_tools.py +0 -0
  136. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/spida_ui.py +0 -0
  137. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/temperature_tools.py +0 -0
  138. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/temperature_ui.py +0 -0
  139. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/temporal_enhancement_tools.py +0 -0
  140. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/timeseries_condensate_tools.py +0 -0
  141. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/timeseries_invitro_fluor_ui.py +0 -0
  142. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/timeseries_invitro_tools.py +0 -0
  143. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/topology_tools.py +0 -0
  144. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/trackmate_bridge.py +0 -0
  145. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/ts_cache_manager.py +0 -0
  146. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/ts_cellpose_tools.py +0 -0
  147. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/two_channel_coloc_tools.py +0 -0
  148. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/video_export_tools.py +0 -0
  149. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/zstack_segmentation_tools.py +0 -0
  150. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/toolbox/zstack_segmentation_ui.py +0 -0
  151. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/__init__.py +0 -0
  152. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/coordinate_readout.py +0 -0
  153. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/field_status.py +0 -0
  154. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_analysis_mixin.py +0 -0
  155. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_diagnostics_mixin.py +0 -0
  156. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_filtering_mixin.py +0 -0
  157. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_imageops_mixin.py +0 -0
  158. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_labels_mixin.py +0 -0
  159. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_modules.py +0 -0
  160. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_segmentation_mixin.py +0 -0
  161. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/ui_utils.py +0 -0
  162. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/ui/workflow_checklist.py +0 -0
  163. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/utils/__init__.py +0 -0
  164. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/utils/channel_naming.py +0 -0
  165. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/utils/general_utils.py +0 -0
  166. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/utils/logging_utils.py +0 -0
  167. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/src/pycat/utils/math_utils.py +0 -0
  168. {pycat_napari-1.5.331 → pycat_napari-1.5.332}/tests/README.md +0 -0
@@ -4,6 +4,42 @@ All notable changes to PyCAT-Napari will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.5.332] - 2026-07-10
8
+ ### Fixed — VPT classifier green↔yellow flicker on bright, well-matched beads
9
+ - **A bright, high-NCC bead no longer flips singlet↔out_of_plane frame-to-frame.**
10
+ The out_of_plane (yellow) class used a per-frame amplitude/SNR percentile, so
11
+ when the bead population is uniformly low-quality a genuinely good bead sitting
12
+ near the moving percentile line was demoted ~a quarter of the time — driven
13
+ mainly by a `low-SNR OR` clause that could yellow a bead whose amplitude was
14
+ fine. Fix: (1) require the AMPLITUDE to actually be low for the dim class (SNR
15
+ is now only a secondary confirmation, never demotes a bright bead on its own),
16
+ and (2) add a high-NCC singlet guard (NCC ≥ 0.80) so a well-matched bead is
17
+ immune to the dim percentile. Verified on real data: the previously-flickering
18
+ bead (amp~164, NCC~0.94) is now singlet in 1000/1000 frames (was ~76%), while a
19
+ genuinely dim bead (amp~75, NCC~0.76) correctly STAYS out_of_plane — the garbage
20
+ rejection (NCC floor) and aggregate class are untouched. This preserves the
21
+ hard-won hot-pixel/ring/noise rejection while stopping the erroneous demotion of
22
+ real beads.
23
+
24
+ ### Added — MSD lag-window fit gate (hardware-defensible fit bounds)
25
+ - `fit_anomalous_diffusion` now computes a **defensible MSD lag window** bounded
26
+ by the frame rate (high-frequency cutoff = frame interval) and the acquisition
27
+ duration (low-frequency cutoff), and by default confines the D/α fit to it.
28
+ Fitting outside this band (only sub-second lags, dominated by the localization
29
+ floor, or out toward the full duration, where a handful of pairs dominate)
30
+ produces a wrong D/α. Exposed under the VPT "Show advanced fit / moduli options"
31
+ expander:
32
+ - **Upper-lag rule** (user-selectable, with tooltips): *Fraction of track length*
33
+ (default, 0.25), *Fixed frequency window* (set the upper lag in seconds), or
34
+ *Minimum independent pairs* (keep a lag only while ≥ N independent tracks span
35
+ it).
36
+ - **"Confine fit to scientifically defensible bounds"** toggle (default ON) —
37
+ clips the fit to the window; turn off to fit the full range at your own risk.
38
+ - The gate **warns, never blocks**: if the acquisition can't cover the requested
39
+ window (too-short clip), it emits a clear warning and falls back gracefully.
40
+ Validated on real data across all three rules + the confine toggle + the
41
+ too-short-data warning path.
42
+
7
43
  ## [1.5.331] - 2026-07-10
8
44
  ### Added — Evans (2009) viscoelastic moduli + bootstrap confidence bands
9
45
  - **G′/G″ (storage/loss moduli) now use the Evans et al. (2009) direct
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycat-napari
3
- Version: 1.5.331
3
+ Version: 1.5.332
4
4
  Summary: Python Condensate Analysis Toolbox - A napari-based tool for biomolecular condensate analysis
5
5
  Project-URL: Homepage, https://github.com/BanerjeeLab-repertoire/pycat-napari
6
6
  Project-URL: Repository, https://github.com/BanerjeeLab-repertoire/pycat-napari
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pycat-napari"
7
- version = "1.5.331"
7
+ version = "1.5.332"
8
8
  description = "Python Condensate Analysis Toolbox - A napari-based tool for biomolecular condensate analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -221,15 +221,55 @@ def fit_anomalous_diffusion(
221
221
  msd_df: pd.DataFrame,
222
222
  max_lag_fit: int = None,
223
223
  fit_localization_offset: bool = True,
224
+ frame_interval_s: float = None,
225
+ upper_lag_rule: str = 'fraction',
226
+ upper_lag_fraction: float = 0.25,
227
+ upper_lag_fixed_s: float = None,
228
+ min_independent_pairs: int = 10,
229
+ confine_to_defensible_bounds: bool = True,
224
230
  ) -> dict:
225
231
  """
226
232
  Fit MSD(τ) = 4D·τ^α (anomalous diffusion model) using log-log regression.
227
233
 
234
+ LAG-WINDOW FIT GATE
235
+ -------------------
236
+ The reliable MSD lag window is bounded by hardware on both ends:
237
+
238
+ * **High-frequency cutoff = frame rate.** The shortest resolvable lag is one
239
+ frame interval; nothing faster is sampled.
240
+ * **Low-frequency cutoff = acquisition duration.** At long lags there are very
241
+ few independent displacement pairs, so the MSD becomes unreliable well
242
+ before the full record length.
243
+
244
+ Fitting outside this band (e.g. only the first fraction of a second, where the
245
+ curve is dominated by the localization-noise floor, or out toward the full
246
+ duration, where a handful of pairs dominate) produces a wrong D/α. The gate
247
+ computes a defensible ``[lag_lo, lag_hi]`` and, when
248
+ ``confine_to_defensible_bounds`` is on (default), fits only within it. It
249
+ **warns rather than blocks** when the data can't cover the requested window.
250
+
228
251
  Parameters
229
252
  ----------
230
- msd_df : output of compute_msd()
231
- max_lag_fit : number of lag points to include in fit.
232
- Default: all points with n_pairs > 10.
253
+ msd_df : output of compute_msd() (needs lag_s, msd_um2, n_pairs; n_tracks used
254
+ for the min-pairs rule when present).
255
+ max_lag_fit : legacy cap on the number of head lags (kept for back-compat;
256
+ applied before the window gate if given).
257
+ fit_localization_offset : fit the +4σ_loc² localization-offset term.
258
+ frame_interval_s : seconds per frame; sets the high-frequency cutoff
259
+ (lag_lo = frame_interval_s). If None, inferred from the smallest lag.
260
+ upper_lag_rule : how to set the low-frequency (upper-lag) cutoff:
261
+ * 'fraction' — lag_hi = upper_lag_fraction × (max track duration).
262
+ The standard convention; conservative.
263
+ * 'fixed' — lag_hi = upper_lag_fixed_s (a hardware-defensible band,
264
+ e.g. matching routine lab practice).
265
+ * 'min_pairs' — keep lags while ≥ min_independent_pairs independent
266
+ tracks span them (statistically principled; adapts to
267
+ how many/how long the tracks are).
268
+ upper_lag_fraction : fraction for the 'fraction' rule (default 0.25).
269
+ upper_lag_fixed_s : upper lag (s) for the 'fixed' rule.
270
+ min_independent_pairs : threshold for the 'min_pairs' rule.
271
+ confine_to_defensible_bounds : if True (default), clip the fit to the computed
272
+ window; if False, fit the full available range (at the user's risk).
233
273
 
234
274
  Returns
235
275
  -------
@@ -242,15 +282,67 @@ def fit_anomalous_diffusion(
242
282
  fit_msd : fitted MSD values (array)
243
283
  log_log_slope : raw slope from log-log regression (= alpha)
244
284
  log_log_intercept : raw intercept (log(4D))
285
+ fit_window_s : (lag_lo, lag_hi) the defensible window used
286
+ fit_window_warning : str or None — set when data can't cover the window
245
287
  """
246
288
  df = msd_df[msd_df['n_pairs'] > 5].copy()
247
289
  if max_lag_fit is not None:
248
290
  df = df.head(max_lag_fit)
291
+
292
+ # ── Lag-window fit gate ──────────────────────────────────────────────────
293
+ window_warning = None
294
+ lag_lo = lag_hi = None
295
+ if 'lag_s' in df.columns and len(df):
296
+ all_lags = df['lag_s'].values.astype(float)
297
+ # High-frequency cutoff = one frame interval.
298
+ lag_lo = float(frame_interval_s) if (frame_interval_s and frame_interval_s > 0) \
299
+ else float(np.min(all_lags))
300
+ # Low-frequency (upper-lag) cutoff per the chosen rule.
301
+ max_lag_available = float(np.max(all_lags))
302
+ rule = (upper_lag_rule or 'fraction').lower()
303
+ if rule == 'fixed' and upper_lag_fixed_s and upper_lag_fixed_s > 0:
304
+ lag_hi = float(upper_lag_fixed_s)
305
+ elif rule == 'min_pairs' and 'n_tracks' in df.columns:
306
+ ok = df[df['n_tracks'] >= int(min_independent_pairs)]
307
+ lag_hi = float(ok['lag_s'].max()) if len(ok) else lag_lo
308
+ else: # 'fraction' (default)
309
+ lag_hi = float(upper_lag_fraction) * max_lag_available
310
+
311
+ # Sanity + coverage warnings (warn, never block).
312
+ if lag_hi <= lag_lo:
313
+ window_warning = (
314
+ f"Requested lag window collapses (lag_lo={lag_lo:.3g}s ≥ "
315
+ f"lag_hi={lag_hi:.3g}s). The acquisition is too short, or the "
316
+ f"upper-lag rule is too strict, to define a fit band. Fitting the "
317
+ f"full available range instead.")
318
+ lag_hi = max_lag_available
319
+ elif lag_hi > max_lag_available + 1e-12:
320
+ window_warning = (
321
+ f"Requested upper lag ({lag_hi:.3g}s) exceeds the longest "
322
+ f"available lag ({max_lag_available:.3g}s): the acquisition "
323
+ f"duration is too short to reach the low-frequency cutoff, so "
324
+ f"G(τ)/viscosity may be under-resolved at long lags.")
325
+ lag_hi = max_lag_available
326
+
327
+ if confine_to_defensible_bounds:
328
+ gated = df[(df['lag_s'] >= lag_lo - 1e-12)
329
+ & (df['lag_s'] <= lag_hi + 1e-12)]
330
+ if len(gated) >= 3:
331
+ df = gated
332
+ else:
333
+ window_warning = (
334
+ (window_warning + " ") if window_warning else ""
335
+ ) + (f"Only {len(gated)} lag(s) fall inside the defensible "
336
+ f"window [{lag_lo:.3g}, {lag_hi:.3g}]s — too few to fit; "
337
+ f"using the full available range instead.")
338
+
249
339
  if len(df) < 3:
250
340
  return dict(D_um2_per_s=np.nan, alpha=np.nan, motion_type='unknown',
251
341
  r_squared=np.nan, fit_lags_s=np.array([]),
252
342
  fit_msd=np.array([]), log_log_slope=np.nan,
253
- log_log_intercept=np.nan)
343
+ log_log_intercept=np.nan,
344
+ fit_window_s=(lag_lo, lag_hi),
345
+ fit_window_warning=window_warning)
254
346
 
255
347
  tau = df['lag_s'].values.astype(float)
256
348
  msd = df['msd_um2'].values.astype(float)
@@ -332,6 +424,8 @@ def fit_anomalous_diffusion(
332
424
  fit_msd=msd_fit,
333
425
  log_log_slope=a_ll,
334
426
  log_log_intercept=log_intercept,
427
+ fit_window_s=(lag_lo, lag_hi),
428
+ fit_window_warning=window_warning,
335
429
  )
336
430
 
337
431
 
@@ -651,24 +651,6 @@ def score_beads_template(frame, coords, template_z, half=4, subpixel=False):
651
651
  amplitude, integrated_intensity.
652
652
  """
653
653
  raw = np.asarray(frame, dtype=np.float32)
654
- # Guard against a degenerate (non-2D) frame reaching here. A multi-file
655
- # OME-TIFF with a MISSING linked file (the loader zeros/truncates it) or a
656
- # collapsed lazy stack can yield a 1-D array, which would crash on
657
- # `H, W = raw.shape` with an opaque "not enough values to unpack" error.
658
- # Squeeze and, if still not 2-D, skip scoring this frame with a clear warning
659
- # instead of taking down the whole detection run.
660
- raw = np.squeeze(raw)
661
- if raw.ndim != 2:
662
- try:
663
- napari_show_warning(
664
- f"Bead scoring skipped a frame with unexpected shape "
665
- f"{np.asarray(frame).shape} (a linked OME-TIFF file may be "
666
- f"missing, or the stack collapsed). Check the file set is complete.")
667
- except Exception:
668
- pass
669
- return [dict(y=float(y), x=float(x), ncc=np.nan, snr=np.nan,
670
- symmetry=np.nan, amplitude=np.nan,
671
- integrated_intensity=np.nan) for (y, x) in coords]
672
654
  H, W = raw.shape
673
655
  w = 2 * half + 1
674
656
  out = []
@@ -830,27 +812,42 @@ def classify_beads(beads_df: pd.DataFrame,
830
812
  else:
831
813
  snr_dim = -np.inf
832
814
 
815
+ # High-NCC guard against out_of_plane flicker. The out_of_plane (yellow)
816
+ # class is meant for genuinely dim / defocused beads — but a per-frame
817
+ # amplitude/SNR percentile will always bin ~the lowest quarter as dim,
818
+ # and when the whole population is uniformly low-quality a BRIGHT,
819
+ # well-matched bead sitting near that moving line flips singlet<->yellow
820
+ # frame-to-frame (verified: a bead at amp~163, ncc~0.95 read out_of_plane
821
+ # ~24% of frames purely from the SNR percentile wobbling). NCC is the
822
+ # template match built FROM the real beads, so a high NCC is strong
823
+ # evidence of a real in-focus bead regardless of a noisy per-frame SNR.
824
+ # A bead with NCC at/above this guard is never demoted to out_of_plane.
825
+ NCC_SINGLET_GUARD = 0.80
826
+
833
827
  n_units, classes = [], []
834
828
  for k in range(len(df)):
835
829
  if not is_real[k]:
836
830
  n_units.append(np.nan); classes.append('rejected'); continue
837
831
  I, A = ii[k], amp[k]
838
832
  S = snr[k] if 'snr' in df else np.nan
833
+ C = ncc[k]
839
834
  nu = I / singlet_int if (singlet_int and singlet_int > 0) else np.nan
840
835
  n_units.append(nu)
841
836
  high_mass = np.isfinite(I) and I >= mass_hi
842
837
  bright = np.isfinite(A) and A >= amp_hi
843
- # Dim / out-of-focus: low amplitude AND not part of the bright
844
- # aggregate tail. These go YELLOW (out_of_plane). Blinking of this
845
- # population across frames is expected and acceptable — a bead that
846
- # is genuinely in focus and stable will instead read as a singlet;
847
- # the temporal-stability pass (after linking) promotes stable dim
848
- # tracks back to singlet.
849
- is_dim = (np.isfinite(A) and A <= amp_dim) or \
850
- (np.isfinite(S) and S <= snr_dim)
838
+ # Dim / out-of-focus. Require the AMPLITUDE to actually be low — a
839
+ # low per-frame SNR alone must NOT demote a bead whose amplitude is
840
+ # fine (that was the flicker source). SNR is now only a secondary
841
+ # confirmation: it can push a bead that is ALREADY amplitude-dim into
842
+ # the yellow bin, but it can't drag a bright bead there on its own.
843
+ amp_low = np.isfinite(A) and A <= amp_dim
844
+ snr_low = np.isfinite(S) and S <= snr_dim
845
+ is_dim = amp_low or (snr_low and amp_low)
846
+ # High-NCC well-matched beads are immune to the dim gate (anti-flicker).
847
+ well_matched = np.isfinite(C) and C >= NCC_SINGLET_GUARD
851
848
  if high_mass and bright:
852
849
  classes.append('aggregate') # bright + compact + heavy
853
- elif is_dim and not high_mass:
850
+ elif is_dim and not high_mass and not well_matched:
854
851
  classes.append('out_of_plane') # YELLOW: dim / out of focus
855
852
  elif high_mass and not bright:
856
853
  classes.append('ambiguous') # heavy but dim/diffuse (OOF)
@@ -867,6 +864,7 @@ def classify_beads(beads_df: pd.DataFrame,
867
864
  df.attrs['classify_thresholds'] = {
868
865
  'mode': 'fast_template',
869
866
  'ncc_floor': float(NCC_FLOOR),
867
+ 'ncc_singlet_guard': float(NCC_SINGLET_GUARD),
870
868
  'aggregate_mass_percentile': 99.3,
871
869
  'aggregate_amp_percentile': 50.0,
872
870
  'aggregate_mass_hi': float(mass_hi),
@@ -1077,9 +1077,56 @@ class VideoParticleTrackingUI:
1077
1077
  _mb.setContentsMargins(0, 0, 0, 0); _mb.setSpacing(5)
1078
1078
  _mb.addRow(self._moduli_boot)
1079
1079
  _mb.addRow("Bootstrap resamples:", self._moduli_nboot)
1080
+
1081
+ # ── Lag-window fit gate (MSD → D/α fit range) ────────────────────────
1082
+ # The reliable MSD lag window is bounded by hardware: high-frequency
1083
+ # cutoff = frame interval, low-frequency cutoff = acquisition duration.
1084
+ # Fitting outside it gives a wrong D/α. These pick the upper-lag rule and
1085
+ # whether to clip the fit to the defensible band (warn, not block).
1086
+ self._lag_rule = QComboBox()
1087
+ self._lag_rule.addItem("Fraction of track length", "fraction")
1088
+ self._lag_rule.addItem("Fixed frequency window", "fixed")
1089
+ self._lag_rule.addItem("Minimum independent pairs", "min_pairs")
1090
+ self._lag_rule.setToolTip(
1091
+ "How to set the upper-lag (low-frequency) cutoff of the MSD fit:\n"
1092
+ "• Fraction of track length — fit lags up to a fraction of the record "
1093
+ "length; longer lags have too few independent samples. Standard, "
1094
+ "conservative.\n"
1095
+ "• Fixed frequency window — restrict to a hardware-defensible band "
1096
+ "(set the upper lag in seconds). Matches routine lab practice.\n"
1097
+ "• Minimum independent pairs — keep a lag only while enough independent "
1098
+ "trajectories span it. Adapts to how many/how long your tracks are.")
1099
+ self._lag_fraction = QDoubleSpinBox()
1100
+ self._lag_fraction.setRange(0.02, 1.0); self._lag_fraction.setValue(0.25)
1101
+ self._lag_fraction.setDecimals(2); self._lag_fraction.setSingleStep(0.05)
1102
+ self._lag_fraction.setToolTip(
1103
+ "Upper lag = this fraction × longest track duration (Fraction rule).")
1104
+ self._lag_fixed_s = QDoubleSpinBox()
1105
+ self._lag_fixed_s.setRange(0.001, 1e6); self._lag_fixed_s.setValue(10.0)
1106
+ self._lag_fixed_s.setDecimals(3)
1107
+ self._lag_fixed_s.setToolTip(
1108
+ "Upper lag in seconds (Fixed-window rule). E.g. 10 s for a "
1109
+ "0.1–10 s defensible band at 0.1 s/frame.")
1110
+ self._lag_minpairs = QSpinBox()
1111
+ self._lag_minpairs.setRange(2, 1000); self._lag_minpairs.setValue(10)
1112
+ self._lag_minpairs.setToolTip(
1113
+ "Keep lags spanned by at least this many independent tracks "
1114
+ "(Minimum-independent-pairs rule).")
1115
+ self._lag_confine = QCheckBox("Confine fit to scientifically defensible bounds")
1116
+ self._lag_confine.setChecked(True)
1117
+ self._lag_confine.setToolTip(
1118
+ "When ON (default), the MSD fit is clipped to the hardware-defensible "
1119
+ "lag window (frame interval → the upper-lag rule above). When OFF, the "
1120
+ "full available lag range is fit at your own risk. Either way, PyCAT "
1121
+ "WARNS (never blocks) if the acquisition can't cover the window.")
1122
+ _mb.addRow("Upper-lag rule:", self._lag_rule)
1123
+ _mb.addRow(" fraction:", self._lag_fraction)
1124
+ _mb.addRow(" fixed upper lag (s):", self._lag_fixed_s)
1125
+ _mb.addRow(" min independent pairs:", self._lag_minpairs)
1126
+ _mb.addRow(self._lag_confine)
1080
1127
  self._moduli_boot_row.setVisible(False) # hidden until toggled
1081
1128
 
1082
- self._show_moduli_adv = QCheckBox("Show advanced moduli (G′/G″) options")
1129
+ self._show_moduli_adv = QCheckBox("Show advanced fit / moduli options")
1083
1130
  self._show_moduli_adv.setChecked(False)
1084
1131
  self._show_moduli_adv.toggled.connect(self._moduli_boot_row.setVisible)
1085
1132
  form.addRow(self._show_moduli_adv)
@@ -1126,7 +1173,29 @@ class VideoParticleTrackingUI:
1126
1173
  tracks,
1127
1174
  frame_interval_s=self._frame_dt.value(),
1128
1175
  min_track_length=self._min_track.value())
1129
- fit = fit_anomalous_diffusion(msd_df)
1176
+ fit = fit_anomalous_diffusion(
1177
+ msd_df,
1178
+ frame_interval_s=self._frame_dt.value(),
1179
+ upper_lag_rule=(self._lag_rule.currentData()
1180
+ if hasattr(self, '_lag_rule') else 'fraction'),
1181
+ upper_lag_fraction=(self._lag_fraction.value()
1182
+ if hasattr(self, '_lag_fraction') else 0.25),
1183
+ upper_lag_fixed_s=(self._lag_fixed_s.value()
1184
+ if hasattr(self, '_lag_fixed_s') else None),
1185
+ min_independent_pairs=(self._lag_minpairs.value()
1186
+ if hasattr(self, '_lag_minpairs') else 10),
1187
+ confine_to_defensible_bounds=(
1188
+ self._lag_confine.isChecked()
1189
+ if hasattr(self, '_lag_confine') else True))
1190
+ _fw = fit.get('fit_window_warning')
1191
+ if _fw:
1192
+ napari_show_warning(f"MSD fit window: {_fw}")
1193
+ print(f"[PyCAT VPT] MSD fit-window note: {_fw}")
1194
+ _win = fit.get('fit_window_s')
1195
+ if _win and all(v is not None for v in _win):
1196
+ print(f"[PyCAT VPT] MSD fit confined to defensible lag window "
1197
+ f"{_win[0]:.3g}–{_win[1]:.3g}s "
1198
+ f"({'confined' if (self._lag_confine.isChecked() if hasattr(self,'_lag_confine') else True) else 'full range (confine off)'}).")
1130
1199
  eta = viscosity_from_diffusion(
1131
1200
  fit.get('D_um2_per_s', float('nan')),
1132
1201
  self._bead_radius.value(), self._temp_C.value())
File without changes
File without changes