openflo 2.6.0__tar.gz → 2.6.2__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 (403) hide show
  1. {openflo-2.6.0 → openflo-2.6.2}/CHANGELOG.md +322 -13
  2. {openflo-2.6.0 → openflo-2.6.2}/MANIFEST.in +3 -0
  3. {openflo-2.6.0/src/openflo.egg-info → openflo-2.6.2}/PKG-INFO +102 -4
  4. {openflo-2.6.0 → openflo-2.6.2}/README.md +101 -3
  5. {openflo-2.6.0 → openflo-2.6.2}/pyproject.toml +11 -1
  6. openflo-2.6.2/scripts/bench_import_cost.py +79 -0
  7. {openflo-2.6.0 → openflo-2.6.2}/scripts/make_synthetic_dataset.py +1 -1
  8. {openflo-2.6.0 → openflo-2.6.2}/scripts/run_analyses.py +1 -1
  9. {openflo-2.6.0 → openflo-2.6.2}/scripts/smoke_test.py +3 -3
  10. openflo-2.6.2/scripts/typecheck.py +275 -0
  11. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/__init__.py +10 -5
  12. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/_golden.json +1 -1
  13. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/calibration.py +53 -17
  14. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/cli.py +1327 -276
  15. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/comp_qc.py +48 -19
  16. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/compare.py +147 -18
  17. openflo-2.6.2/src/openflo/diffexp.py +406 -0
  18. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_analysis.py +40 -12
  19. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_autoclean.py +39 -11
  20. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_autogate.py +9 -1
  21. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_channels.py +32 -0
  22. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_chrome.py +6 -2
  23. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_clipboard.py +71 -11
  24. openflo-2.6.2/src/openflo/editor_compute.py +583 -0
  25. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_dnd.py +26 -9
  26. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_downsample.py +1 -0
  27. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_export.py +26 -14
  28. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_figure.py +2 -2
  29. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_gatetools.py +227 -16
  30. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_gating.py +323 -49
  31. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_grouping.py +4 -2
  32. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_lifecycle.py +6 -0
  33. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_load.py +168 -21
  34. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_loadpool.py +222 -4
  35. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_plot.py +188 -146
  36. openflo-2.6.2/src/openflo/editor_plotcache.py +1050 -0
  37. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_populations.py +58 -28
  38. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_session.py +76 -4
  39. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_slider.py +46 -10
  40. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_stats.py +35 -10
  41. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_template.py +59 -9
  42. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_tools.py +132 -17
  43. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_tree.py +31 -6
  44. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_undo.py +11 -1
  45. openflo-2.6.2/src/openflo/fcs_export.py +182 -0
  46. openflo-2.6.2/src/openflo/gating.py +360 -0
  47. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/gating_helpers.py +3 -3
  48. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/gui.py +94 -41
  49. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/interop.py +46 -6
  50. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/pipeline.py +3323 -504
  51. openflo-2.6.2/src/openflo/plotcache.py +405 -0
  52. openflo-2.6.2/src/openflo/plotmath.py +393 -0
  53. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/prefs.py +8 -0
  54. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/preview.py +19 -19
  55. openflo-2.6.2/src/openflo/scales.py +396 -0
  56. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/selftest.py +3 -3
  57. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/selftest_controls.py +1 -1
  58. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/session_format.py +40 -0
  59. openflo-2.6.2/src/openflo/spectral.py +927 -0
  60. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/stats.py +39 -5
  61. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/synthetic.py +32 -32
  62. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/template_library/README.md +17 -10
  63. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/template_library/cleanup_minimal.json +1 -1
  64. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/template_library/cleanup_standard.json +2 -2
  65. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/template_library/cleanup_strict.json +1 -1
  66. openflo-2.6.2/src/openflo/template_library/example_panel.json +15 -0
  67. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/trajectory.py +4 -4
  68. openflo-2.6.2/src/openflo/ui_abscounts.py +144 -0
  69. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_axis_config.py +15 -3
  70. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_calibration.py +27 -7
  71. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_cell_cycle.py +5 -2
  72. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_comp.py +172 -87
  73. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_compare.py +6 -2
  74. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_diff.py +81 -24
  75. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_expression.py +124 -23
  76. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_figure_layout.py +1 -1
  77. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_fmo.py +38 -6
  78. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_frequency.py +136 -21
  79. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_group_stats.py +32 -7
  80. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_preferences.py +19 -0
  81. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_sample_qc.py +14 -3
  82. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_spectral_qc.py +46 -2
  83. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_statistics.py +42 -9
  84. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_trajectory.py +35 -8
  85. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/update.py +29 -4
  86. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/voltage.py +2 -2
  87. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/workspace.py +235 -26
  88. {openflo-2.6.0 → openflo-2.6.2/src/openflo.egg-info}/PKG-INFO +102 -4
  89. {openflo-2.6.0 → openflo-2.6.2}/src/openflo.egg-info/SOURCES.txt +90 -2
  90. {openflo-2.6.0 → openflo-2.6.2}/templates/testtemplate.json +2 -2
  91. openflo-2.6.2/tests/conftest.py +449 -0
  92. openflo-2.6.2/tests/test_abscounts.py +148 -0
  93. openflo-2.6.2/tests/test_acquisition_qc_ground_truth.py +462 -0
  94. {openflo-2.6.0 → openflo-2.6.2}/tests/test_annotate.py +6 -6
  95. openflo-2.6.2/tests/test_annotate_clusters.py +137 -0
  96. openflo-2.6.2/tests/test_annotate_rename_dialogs.py +103 -0
  97. openflo-2.6.2/tests/test_annotation_window.py +107 -0
  98. {openflo-2.6.0 → openflo-2.6.2}/tests/test_apply_region_gates.py +3 -3
  99. {openflo-2.6.0 → openflo-2.6.2}/tests/test_autoclean_counts.py +15 -3
  100. openflo-2.6.2/tests/test_autoclean_editor_actions.py +330 -0
  101. {openflo-2.6.0 → openflo-2.6.2}/tests/test_autoclean_methods.py +289 -15
  102. openflo-2.6.2/tests/test_autogate_dialog.py +201 -0
  103. openflo-2.6.2/tests/test_axis_intensity_ticks.py +175 -0
  104. {openflo-2.6.0 → openflo-2.6.2}/tests/test_boolean_gate_fails_closed.py +78 -0
  105. openflo-2.6.2/tests/test_boolean_gate_tools.py +698 -0
  106. openflo-2.6.2/tests/test_calibration_dialog.py +155 -0
  107. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cell_cycle.py +5 -5
  108. openflo-2.6.2/tests/test_cell_cycle_dialog.py +181 -0
  109. openflo-2.6.2/tests/test_channel_classification.py +93 -0
  110. openflo-2.6.2/tests/test_cli_export_wsp.py +53 -0
  111. openflo-2.6.2/tests/test_cli_run.py +1215 -0
  112. openflo-2.6.2/tests/test_cli_run_options.py +143 -0
  113. openflo-2.6.2/tests/test_cli_shared_clusters.py +316 -0
  114. openflo-2.6.2/tests/test_cluster_dialog.py +629 -0
  115. openflo-2.6.2/tests/test_cluster_gpu_fallback.py +170 -0
  116. openflo-2.6.2/tests/test_cluster_heatmap.py +72 -0
  117. openflo-2.6.2/tests/test_clustering_known_defects.py +157 -0
  118. {openflo-2.6.0 → openflo-2.6.2}/tests/test_comp_matrix_csv_quoting.py +12 -12
  119. {openflo-2.6.0 → openflo-2.6.2}/tests/test_comp_qc.py +64 -6
  120. {openflo-2.6.0 → openflo-2.6.2}/tests/test_compare.py +17 -12
  121. {openflo-2.6.0 → openflo-2.6.2}/tests/test_compare_conditions_absent_clusters.py +5 -0
  122. openflo-2.6.2/tests/test_compare_conditions_joint_clustering.py +103 -0
  123. openflo-2.6.2/tests/test_compare_dialog.py +134 -0
  124. openflo-2.6.2/tests/test_compare_flowjo_semantics.py +219 -0
  125. {openflo-2.6.0 → openflo-2.6.2}/tests/test_compensation.py +1 -1
  126. openflo-2.6.2/tests/test_compensation_applied_once.py +352 -0
  127. openflo-2.6.2/tests/test_compensation_editor_apply.py +442 -0
  128. openflo-2.6.2/tests/test_compensation_fcs_spill.py +178 -0
  129. openflo-2.6.2/tests/test_compensation_matrix_reading.py +258 -0
  130. {openflo-2.6.0 → openflo-2.6.2}/tests/test_diff_abundance_small_n.py +24 -13
  131. openflo-2.6.2/tests/test_diffabundance_calibration.py +196 -0
  132. {openflo-2.6.0 → openflo-2.6.2}/tests/test_diffexp.py +5 -4
  133. openflo-2.6.2/tests/test_editor_tools.py +381 -0
  134. openflo-2.6.2/tests/test_ellipse_params_geometry.py +62 -0
  135. openflo-2.6.2/tests/test_ellipse_tool.py +392 -0
  136. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ellipsoid_quadrant.py +120 -46
  137. {openflo-2.6.0 → openflo-2.6.2}/tests/test_embedding.py +19 -19
  138. openflo-2.6.2/tests/test_embedding_dialog.py +104 -0
  139. openflo-2.6.2/tests/test_expression_window.py +259 -0
  140. {openflo-2.6.0 → openflo-2.6.2}/tests/test_fcs_export.py +8 -8
  141. {openflo-2.6.0 → openflo-2.6.2}/tests/test_figure_layout.py +12 -12
  142. openflo-2.6.2/tests/test_flowsom_tree_window.py +94 -0
  143. openflo-2.6.2/tests/test_fmo_gating_dialog.py +157 -0
  144. openflo-2.6.2/tests/test_fmo_threshold_log.py +35 -0
  145. openflo-2.6.2/tests/test_fold_change_direction.py +92 -0
  146. openflo-2.6.2/tests/test_frequency_missing_populations.py +260 -0
  147. openflo-2.6.2/tests/test_frequency_window.py +327 -0
  148. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_editor_helpers.py +13 -6
  149. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_import_fixes.py +16 -12
  150. openflo-2.6.2/tests/test_gate_text_intensity.py +467 -0
  151. openflo-2.6.2/tests/test_gate_tools_axis_ends.py +307 -0
  152. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gating.py +2 -2
  153. openflo-2.6.2/tests/test_gmm_ellipse_info.py +62 -0
  154. {openflo-2.6.0 → openflo-2.6.2}/tests/test_golden_regression.py +12 -12
  155. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gpu_accel.py +17 -0
  156. openflo-2.6.2/tests/test_group_stats_window.py +147 -0
  157. {openflo-2.6.0 → openflo-2.6.2}/tests/test_grouping.py +2 -2
  158. openflo-2.6.2/tests/test_gui_guard.py +99 -0
  159. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gui_smoke.py +87 -15
  160. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gui_ux.py +134 -0
  161. {openflo-2.6.0 → openflo-2.6.2}/tests/test_import_is_lazy.py +39 -11
  162. openflo-2.6.2/tests/test_inspect_fcs.py +65 -0
  163. {openflo-2.6.0 → openflo-2.6.2}/tests/test_interop_h5ad_roundtrip.py +43 -0
  164. openflo-2.6.2/tests/test_label_align.py +148 -0
  165. {openflo-2.6.0 → openflo-2.6.2}/tests/test_no_plausible_fills.py +2 -10
  166. openflo-2.6.2/tests/test_optimize_compensation_truth.py +129 -0
  167. {openflo-2.6.0 → openflo-2.6.2}/tests/test_panel_pairs.py +22 -22
  168. openflo-2.6.2/tests/test_parse_ellipsoid.py +909 -0
  169. openflo-2.6.2/tests/test_phenograph_pool_cap.py +82 -0
  170. openflo-2.6.2/tests/test_plot_cache.py +693 -0
  171. openflo-2.6.2/tests/test_plotmath.py +281 -0
  172. openflo-2.6.2/tests/test_polygon_rect_tools.py +422 -0
  173. openflo-2.6.2/tests/test_population_stats_geomean.py +171 -0
  174. openflo-2.6.2/tests/test_preexisting_data_defects.py +314 -0
  175. {openflo-2.6.0 → openflo-2.6.2}/tests/test_preview_quadrants.py +6 -6
  176. {openflo-2.6.0 → openflo-2.6.2}/tests/test_provenance.py +1 -1
  177. {openflo-2.6.0 → openflo-2.6.2}/tests/test_provenance_unreadable_entries.py +1 -1
  178. {openflo-2.6.0 → openflo-2.6.2}/tests/test_qc.py +18 -13
  179. {openflo-2.6.0 → openflo-2.6.2}/tests/test_qc_ignores_derived_columns.py +82 -7
  180. {openflo-2.6.0 → openflo-2.6.2}/tests/test_qc_integer_channels.py +4 -10
  181. openflo-2.6.2/tests/test_qc_removed_visible.py +156 -0
  182. openflo-2.6.2/tests/test_quadrant_tool.py +171 -0
  183. {openflo-2.6.0 → openflo-2.6.2}/tests/test_quality_correctness.py +5 -5
  184. openflo-2.6.2/tests/test_quick_preview_dialog.py +109 -0
  185. {openflo-2.6.0 → openflo-2.6.2}/tests/test_report.py +2 -2
  186. openflo-2.6.2/tests/test_run_louvain.py +98 -0
  187. openflo-2.6.2/tests/test_sample_qc_window_values.py +107 -0
  188. openflo-2.6.2/tests/test_selftest_cli.py +75 -0
  189. {openflo-2.6.0 → openflo-2.6.2}/tests/test_session.py +29 -21
  190. openflo-2.6.2/tests/test_session_restore_transforms.py +215 -0
  191. {openflo-2.6.0 → openflo-2.6.2}/tests/test_spectral_degenerate_reference.py +2 -2
  192. openflo-2.6.2/tests/test_spectral_matched_af.py +339 -0
  193. openflo-2.6.2/tests/test_spectral_measured_and_spread.py +275 -0
  194. openflo-2.6.2/tests/test_spectral_qc_window.py +191 -0
  195. openflo-2.6.2/tests/test_spectral_unmix_gui.py +242 -0
  196. openflo-2.6.2/tests/test_statistics_export.py +232 -0
  197. {openflo-2.6.0 → openflo-2.6.2}/tests/test_stats.py +76 -64
  198. {openflo-2.6.0 → openflo-2.6.2}/tests/test_survivor_boundaries.py +1 -1
  199. {openflo-2.6.0 → openflo-2.6.2}/tests/test_synthetic.py +27 -27
  200. openflo-2.6.2/tests/test_template_apply.py +177 -0
  201. openflo-2.6.2/tests/test_template_one_sample.py +66 -0
  202. openflo-2.6.2/tests/test_threshold_inclusive.py +96 -0
  203. {openflo-2.6.0 → openflo-2.6.2}/tests/test_trajectory.py +1 -1
  204. openflo-2.6.2/tests/test_trajectory_window.py +131 -0
  205. openflo-2.6.2/tests/test_transform_consistency.py +71 -0
  206. {openflo-2.6.0 → openflo-2.6.2}/tests/test_transform_nonfinite.py +3 -3
  207. openflo-2.6.2/tests/test_transform_record_writes.py +140 -0
  208. openflo-2.6.2/tests/test_transform_records.py +590 -0
  209. openflo-2.6.2/tests/test_typecheck_config.py +143 -0
  210. openflo-2.6.2/tests/test_ui_audit_sign_verify.py +134 -0
  211. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_dialogs_smoke.py +3 -3
  212. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_logic.py +5 -5
  213. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_voltage.py +2 -2
  214. {openflo-2.6.0 → openflo-2.6.2}/tests/test_undo_redo.py +2 -2
  215. openflo-2.6.2/tests/test_update_flow.py +225 -0
  216. openflo-2.6.2/tests/test_v261_sessions.py +276 -0
  217. {openflo-2.6.0 → openflo-2.6.2}/tests/test_voltage.py +21 -21
  218. openflo-2.6.2/tests/test_voltage_cli.py +103 -0
  219. openflo-2.6.2/tests/test_voltage_titration_dialog.py +134 -0
  220. {openflo-2.6.0 → openflo-2.6.2}/tests/test_workspace.py +50 -22
  221. openflo-2.6.2/tests/test_workspace_batch_folder.py +111 -0
  222. openflo-2.6.2/tests/test_workspace_job_driver.py +194 -0
  223. {openflo-2.6.0 → openflo-2.6.2}/tests/test_write_fcs.py +4 -4
  224. openflo-2.6.2/tests/test_wsp_axis_floor.py +444 -0
  225. openflo-2.6.2/tests/test_wsp_export_golden.py +119 -0
  226. {openflo-2.6.0 → openflo-2.6.2}/tests/test_wsp_extract.py +4 -2
  227. openflo-2.6.2/tests/test_wsp_gate_units.py +287 -0
  228. {openflo-2.6.0 → openflo-2.6.2}/tests/test_wsp_partial_matrix.py +3 -3
  229. openflo-2.6.2/tests/test_wsp_quadrant_export.py +110 -0
  230. openflo-2.6.2/tests/test_wsp_sample_compensation.py +129 -0
  231. openflo-2.6.2/tests/test_wsp_sample_keywords.py +91 -0
  232. openflo-2.6.2/tests/test_wsp_time_units.py +410 -0
  233. {openflo-2.6.0 → openflo-2.6.2}/tests/test_wsp_writer.py +25 -25
  234. openflo-2.6.2/tests/test_wsp_writer_structure.py +156 -0
  235. openflo-2.6.0/scripts/publish_public.py +0 -198
  236. openflo-2.6.0/src/openflo/diffexp.py +0 -255
  237. openflo-2.6.0/src/openflo/editor_compute.py +0 -218
  238. openflo-2.6.0/src/openflo/fcs_export.py +0 -87
  239. openflo-2.6.0/src/openflo/gating.py +0 -165
  240. openflo-2.6.0/src/openflo/plotmath.py +0 -191
  241. openflo-2.6.0/src/openflo/scales.py +0 -85
  242. openflo-2.6.0/src/openflo/spectral.py +0 -305
  243. openflo-2.6.0/src/openflo/template_library/example_panel.json +0 -15
  244. openflo-2.6.0/src/openflo/ui_abscounts.py +0 -74
  245. openflo-2.6.0/tests/conftest.py +0 -201
  246. openflo-2.6.0/tests/test_abscounts.py +0 -47
  247. openflo-2.6.0/tests/test_label_align.py +0 -148
  248. openflo-2.6.0/tests/test_plotmath.py +0 -102
  249. openflo-2.6.0/tests/test_population_stats_geomean.py +0 -101
  250. {openflo-2.6.0 → openflo-2.6.2}/LICENSE.txt +0 -0
  251. {openflo-2.6.0 → openflo-2.6.2}/scripts/bench_accelerators.py +0 -0
  252. {openflo-2.6.0 → openflo-2.6.2}/scripts/bench_louvain_restarts.py +0 -0
  253. {openflo-2.6.0 → openflo-2.6.2}/scripts/bench_snn_prune.py +0 -0
  254. {openflo-2.6.0 → openflo-2.6.2}/scripts/bench_ui_responsiveness.py +0 -0
  255. {openflo-2.6.0 → openflo-2.6.2}/scripts/bench_umap_knn.py +0 -0
  256. {openflo-2.6.0 → openflo-2.6.2}/scripts/lowest_direct_requirements.py +0 -0
  257. {openflo-2.6.0 → openflo-2.6.2}/scripts/migrate_session.py +0 -0
  258. {openflo-2.6.0 → openflo-2.6.2}/scripts/mutate_controls.py +0 -0
  259. {openflo-2.6.0 → openflo-2.6.2}/scripts/preflight.py +0 -0
  260. {openflo-2.6.0 → openflo-2.6.2}/scripts/theme_audit.py +0 -0
  261. {openflo-2.6.0 → openflo-2.6.2}/setup.cfg +0 -0
  262. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/_console.py +0 -0
  263. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/annotate.py +0 -0
  264. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/async_task.py +0 -0
  265. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/audit.py +0 -0
  266. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/capabilities.py +0 -0
  267. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/compliance.py +0 -0
  268. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/density.py +0 -0
  269. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/diagnostics.py +0 -0
  270. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/dr_compare.py +0 -0
  271. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_audit.py +0 -0
  272. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_base.py +0 -0
  273. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_console.py +0 -0
  274. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_help.py +0 -0
  275. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_menu.py +0 -0
  276. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_mode.py +0 -0
  277. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/editor_update.py +0 -0
  278. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/gatetree.py +0 -0
  279. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/gpu_accel.py +0 -0
  280. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/inspect_fcs.py +0 -0
  281. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/paths.py +0 -0
  282. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/provenance.py +0 -0
  283. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/py.typed +0 -0
  284. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/report.py +0 -0
  285. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/template_library/cleanup_acquisition_qc.json +0 -0
  286. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/theme.py +0 -0
  287. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/tool_window.py +0 -0
  288. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/tree_ids.py +0 -0
  289. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_annotation.py +0 -0
  290. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_audit.py +0 -0
  291. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_autogate.py +0 -0
  292. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_embedding.py +0 -0
  293. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_figure_window.py +0 -0
  294. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_flowsom_tree.py +0 -0
  295. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_inspect.py +0 -0
  296. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_logic.py +0 -0
  297. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_methods.py +0 -0
  298. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_preview.py +0 -0
  299. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_spectral_unmix.py +0 -0
  300. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_synth.py +0 -0
  301. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_tips.py +0 -0
  302. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/ui_voltage.py +0 -0
  303. {openflo-2.6.0 → openflo-2.6.2}/src/openflo/warmup.py +0 -0
  304. {openflo-2.6.0 → openflo-2.6.2}/src/openflo.egg-info/dependency_links.txt +0 -0
  305. {openflo-2.6.0 → openflo-2.6.2}/src/openflo.egg-info/entry_points.txt +0 -0
  306. {openflo-2.6.0 → openflo-2.6.2}/src/openflo.egg-info/requires.txt +0 -0
  307. {openflo-2.6.0 → openflo-2.6.2}/src/openflo.egg-info/top_level.txt +0 -0
  308. {openflo-2.6.0 → openflo-2.6.2}/tests/__init__.py +0 -0
  309. {openflo-2.6.0 → openflo-2.6.2}/tests/test_async_task.py +0 -0
  310. {openflo-2.6.0 → openflo-2.6.2}/tests/test_audit.py +0 -0
  311. {openflo-2.6.0 → openflo-2.6.2}/tests/test_autoclean_doublets_scale.py +0 -0
  312. {openflo-2.6.0 → openflo-2.6.2}/tests/test_autogate.py +0 -0
  313. {openflo-2.6.0 → openflo-2.6.2}/tests/test_autogate_reproducible.py +0 -0
  314. {openflo-2.6.0 → openflo-2.6.2}/tests/test_calibration.py +0 -0
  315. {openflo-2.6.0 → openflo-2.6.2}/tests/test_capabilities.py +0 -0
  316. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cli_cluster.py +0 -0
  317. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cli_e2e.py +0 -0
  318. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cli_unmix.py +0 -0
  319. {openflo-2.6.0 → openflo-2.6.2}/tests/test_clipboard_move_marker.py +0 -0
  320. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cluster_labels.py +0 -0
  321. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cluster_reproducible.py +0 -0
  322. {openflo-2.6.0 → openflo-2.6.2}/tests/test_clustering_reproducible.py +0 -0
  323. {openflo-2.6.0 → openflo-2.6.2}/tests/test_comp_editor.py +0 -0
  324. {openflo-2.6.0 → openflo-2.6.2}/tests/test_compare_all.py +0 -0
  325. {openflo-2.6.0 → openflo-2.6.2}/tests/test_compliance.py +0 -0
  326. {openflo-2.6.0 → openflo-2.6.2}/tests/test_console_encoding.py +0 -0
  327. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cytonorm.py +0 -0
  328. {openflo-2.6.0 → openflo-2.6.2}/tests/test_cytonorm_qc_undefined.py +0 -0
  329. {openflo-2.6.0 → openflo-2.6.2}/tests/test_degenerate_scatter_gating.py +0 -0
  330. {openflo-2.6.0 → openflo-2.6.2}/tests/test_density.py +0 -0
  331. {openflo-2.6.0 → openflo-2.6.2}/tests/test_diagnostics.py +0 -0
  332. {openflo-2.6.0 → openflo-2.6.2}/tests/test_diagnostics_selftest_gate.py +0 -0
  333. {openflo-2.6.0 → openflo-2.6.2}/tests/test_dialog_tooltips.py +0 -0
  334. {openflo-2.6.0 → openflo-2.6.2}/tests/test_diffabundance.py +0 -0
  335. {openflo-2.6.0 → openflo-2.6.2}/tests/test_doctor_tk_check.py +0 -0
  336. {openflo-2.6.0 → openflo-2.6.2}/tests/test_dr_compare.py +0 -0
  337. {openflo-2.6.0 → openflo-2.6.2}/tests/test_drag_tree_refresh.py +0 -0
  338. {openflo-2.6.0 → openflo-2.6.2}/tests/test_editor_mixins.py +0 -0
  339. {openflo-2.6.0 → openflo-2.6.2}/tests/test_embeddings.py +0 -0
  340. {openflo-2.6.0 → openflo-2.6.2}/tests/test_exceptions.py +0 -0
  341. {openflo-2.6.0 → openflo-2.6.2}/tests/test_exit_autosave.py +0 -0
  342. {openflo-2.6.0 → openflo-2.6.2}/tests/test_fast_graph.py +0 -0
  343. {openflo-2.6.0 → openflo-2.6.2}/tests/test_flowsom.py +0 -0
  344. {openflo-2.6.0 → openflo-2.6.2}/tests/test_flowsom_mst.py +0 -0
  345. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_impl_parity.py +0 -0
  346. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_mask_cache.py +0 -0
  347. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_nonfinite_events.py +0 -0
  348. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_partition.py +0 -0
  349. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gate_replication_parent.py +0 -0
  350. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gatetree.py +0 -0
  351. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gating_helpers.py +0 -0
  352. {openflo-2.6.0 → openflo-2.6.2}/tests/test_group_stats.py +0 -0
  353. {openflo-2.6.0 → openflo-2.6.2}/tests/test_group_summary_sd.py +0 -0
  354. {openflo-2.6.0 → openflo-2.6.2}/tests/test_gui_toolings.py +0 -0
  355. {openflo-2.6.0 → openflo-2.6.2}/tests/test_histogram_offscale_note.py +0 -0
  356. {openflo-2.6.0 → openflo-2.6.2}/tests/test_histogram_resolution.py +0 -0
  357. {openflo-2.6.0 → openflo-2.6.2}/tests/test_interop.py +0 -0
  358. {openflo-2.6.0 → openflo-2.6.2}/tests/test_legend_placement.py +0 -0
  359. {openflo-2.6.0 → openflo-2.6.2}/tests/test_leiden.py +0 -0
  360. {openflo-2.6.0 → openflo-2.6.2}/tests/test_load_no_copy.py +0 -0
  361. {openflo-2.6.0 → openflo-2.6.2}/tests/test_loader_queue.py +0 -0
  362. {openflo-2.6.0 → openflo-2.6.2}/tests/test_log_histogram_range.py +0 -0
  363. {openflo-2.6.0 → openflo-2.6.2}/tests/test_log_transform_nonpositive.py +0 -0
  364. {openflo-2.6.0 → openflo-2.6.2}/tests/test_mem_tiny_clusters.py +0 -0
  365. {openflo-2.6.0 → openflo-2.6.2}/tests/test_normalise_groups_idempotent.py +0 -0
  366. {openflo-2.6.0 → openflo-2.6.2}/tests/test_not_gate_excludes_unmeasured.py +0 -0
  367. {openflo-2.6.0 → openflo-2.6.2}/tests/test_paths.py +0 -0
  368. {openflo-2.6.0 → openflo-2.6.2}/tests/test_plot_density.py +0 -0
  369. {openflo-2.6.0 → openflo-2.6.2}/tests/test_polygon_gating.py +0 -0
  370. {openflo-2.6.0 → openflo-2.6.2}/tests/test_preferences_wiring.py +0 -0
  371. {openflo-2.6.0 → openflo-2.6.2}/tests/test_prefs.py +0 -0
  372. {openflo-2.6.0 → openflo-2.6.2}/tests/test_prepare_unit_tags.py +0 -0
  373. {openflo-2.6.0 → openflo-2.6.2}/tests/test_propagate_downsample_order.py +0 -0
  374. {openflo-2.6.0 → openflo-2.6.2}/tests/test_raw_not_aliased.py +0 -0
  375. {openflo-2.6.0 → openflo-2.6.2}/tests/test_resume_and_transfer_hygiene.py +0 -0
  376. {openflo-2.6.0 → openflo-2.6.2}/tests/test_scales.py +0 -0
  377. {openflo-2.6.0 → openflo-2.6.2}/tests/test_scales_logicle.py +0 -0
  378. {openflo-2.6.0 → openflo-2.6.2}/tests/test_seed_phrases.py +0 -0
  379. {openflo-2.6.0 → openflo-2.6.2}/tests/test_selftest.py +0 -0
  380. {openflo-2.6.0 → openflo-2.6.2}/tests/test_selftest_controls.py +0 -0
  381. {openflo-2.6.0 → openflo-2.6.2}/tests/test_session_continuity.py +0 -0
  382. {openflo-2.6.0 → openflo-2.6.2}/tests/test_session_json_is_valid.py +0 -0
  383. {openflo-2.6.0 → openflo-2.6.2}/tests/test_session_migration_machinery.py +0 -0
  384. {openflo-2.6.0 → openflo-2.6.2}/tests/test_session_sidecar_collision.py +0 -0
  385. {openflo-2.6.0 → openflo-2.6.2}/tests/test_spectral.py +0 -0
  386. {openflo-2.6.0 → openflo-2.6.2}/tests/test_spectral_unmix_nonfinite.py +0 -0
  387. {openflo-2.6.0 → openflo-2.6.2}/tests/test_stats_multigroup.py +0 -0
  388. {openflo-2.6.0 → openflo-2.6.2}/tests/test_templates.py +0 -0
  389. {openflo-2.6.0 → openflo-2.6.2}/tests/test_theme.py +0 -0
  390. {openflo-2.6.0 → openflo-2.6.2}/tests/test_tool_window.py +0 -0
  391. {openflo-2.6.0 → openflo-2.6.2}/tests/test_trajectory_nonfinite.py +0 -0
  392. {openflo-2.6.0 → openflo-2.6.2}/tests/test_transforms.py +0 -0
  393. {openflo-2.6.0 → openflo-2.6.2}/tests/test_tree_ids.py +0 -0
  394. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_compare.py +0 -0
  395. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_inspect.py +0 -0
  396. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_preview.py +0 -0
  397. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_sample_qc.py +0 -0
  398. {openflo-2.6.0 → openflo-2.6.2}/tests/test_ui_synth.py +0 -0
  399. {openflo-2.6.0 → openflo-2.6.2}/tests/test_umap_embedding.py +0 -0
  400. {openflo-2.6.0 → openflo-2.6.2}/tests/test_update.py +0 -0
  401. {openflo-2.6.0 → openflo-2.6.2}/tests/test_warmup.py +0 -0
  402. {openflo-2.6.0 → openflo-2.6.2}/tests/test_workspace_prep_offthread.py +0 -0
  403. {openflo-2.6.0 → openflo-2.6.2}/tests/test_wsp_population_names.py +0 -0
@@ -6,6 +6,315 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.6.2] - 2026-10-09
10
+
11
+ ### Changed
12
+ - **Example markers and cutoffs are generic too.** The example panel,
13
+ synthetic data, CLI defaults, docs and tests now use the markers `CD901b`,
14
+ `CD902` and `CD903` (they still parse as CD markers) and round example
15
+ cutoffs (0.5 / 0.4 / 0.45). The privacy gate blocks the real markers.
16
+ - **Examples, fixtures and docs use generic detector names.** Channels in
17
+ the example panel, synthetic data, CLI defaults, README, tests and golden
18
+ fixtures are now `FL1-A`, `FL2-A`, ... instead of real fluorochromes, and
19
+ the comp-control matcher tests use generic dyes (`FLB`, `FLB-Cy7`, ...)
20
+ with the same tandem structure. The privacy gate now blocks the three
21
+ most common real ones, except in a functional fluor catalog
22
+ (`src/openflo/fluor_catalog.py`), which names dyes without revealing a
23
+ panel. Note: a session or template saved against the old example channel
24
+ names will not match the renamed example panel.
25
+ - **Repo tidy for the beta.** Removed four empty files at the repo root
26
+ (`UMAP`, `data`, `events`, `i'`) left by stray shell redirects, and
27
+ CONTRIBUTING.md now points to `src/openflo/pipeline.py` for
28
+ `OpenFloError` instead of the nonexistent `flow_pipeline.py`.
29
+ - **The privacy gate now runs on every PR and before every PyPI upload.**
30
+ It used to run only when the public mirror was refreshed, and the sdist
31
+ is built from this repo, so nothing scrubbed what PyPI would get. That
32
+ gap was real: the sdist shipped `scripts/publish_public.py`, whose
33
+ pattern list names the private tokens the gate exists to block.
34
+ MANIFEST.in now excludes it, `publish_public.py` gains `--check` and
35
+ `--check-dist DIR`, and CI's new `privacy gate` job and the release
36
+ workflow run them.
37
+ - **The side panel is compact, and the plot's controls fit the default
38
+ window.** At 1500×840 the Samples & Gates panel took 747px, half the
39
+ window, and the plot's control bar was cut off ("Color" showed one
40
+ letter).
41
+ - The panel is now 372px and the plot pane 1116px.
42
+ - The action buttons use tighter padding and sit in three columns.
43
+ Undo and Redo share one cell.
44
+ - The Pipeline button reads "Pipeline (beta)".
45
+ - A "Layout (to be added)" button marks the place of a FlowJo-style
46
+ layout editor, which is not built yet. Clicking it says so and offers
47
+ File → Figure layout…, which builds multi-panel figures from preset
48
+ arrangements.
49
+ - Showing the log/console no longer widens the panel.
50
+ - The tree's "All" heading is no longer clipped.
51
+ - **Acquisition QC on load is off by default.** It is a new Preferences
52
+ option: Loading → "Run acquisition QC on load". QC removes events from
53
+ time bins where the signal drifts or the flow rate jumps, and events piled
54
+ at a channel's ceiling, before any gate. FlowJo has no such step, so with
55
+ QC on, the same gates count fewer events in OpenFlo than in FlowJo.
56
+ - Turning the option on applies to files loaded afterwards.
57
+ - A sample keeps the choice it was loaded with when it is re-compensated,
58
+ reopened from a session, moved to another window or reloaded by the
59
+ Workspace.
60
+ - Sessions saved before this change reload their samples with QC, as they
61
+ were loaded.
62
+ - `openflo-run` is unchanged.
63
+ - **The type check gives the same answer locally and in CI** (development
64
+ only; nothing a user runs changes).
65
+ - CI and the pre-push hook now run the same `scripts/typecheck.py`, with
66
+ the same packages: CI also installs the optional backends
67
+ (`requirements-typecheck.txt`), which it used to leave unresolved.
68
+ - The script refuses to run (exit 2) when pyright would read a different
69
+ environment than CI: wrong package versions, a different pyright, or
70
+ pyright resolving imports from another interpreter.
71
+ - In a git worktree it uses the main checkout's `.venv`; no junction needed.
72
+ - A release tag is built only after the same type check passes.
73
+
74
+ ### Fixed
75
+ - **Reference spectra of dim dyes on autofluorescent cells.** The positive
76
+ population of a dim single-stain control was picked by total brightness.
77
+ It then included bright-AF cells, which mixed autofluorescence into the
78
+ dye's spectrum. Example: a dye at 400 on cells where 30% have 10x AF read
79
+ cosine 0.53–0.98 to its true spectrum, and unmixing error was 0.38–0.72.
80
+ - Positives are now picked by the dye's own excess in its primary
81
+ detector. Each event's autofluorescence is subtracted using its nearest
82
+ unstained neighbours in the detectors where AF dominates. The same case
83
+ now reads 0.9999 and unmixing error 0.044–0.061, which matches the
84
+ true-spectrum floor.
85
+ - Guardrail: the old estimate is kept unless the two estimates disagree
86
+ (cosine < 0.99) or the control is poorly separated. Over a 576-cell
87
+ simulation grid, no cell got worse than before. Every bright dye
88
+ (≥10k) and every control without AF variation gives a bit-identical
89
+ result.
90
+ - Each spectrum now says how it was built. Controls too dim to trust are
91
+ named with a recommendation (beads or a brighter fluor), in the CLI
92
+ report, the editor status bar and the Spectral QC window.
93
+ - New `method='auto'|'total'|'matched'` and `beads=` options, and the CLI
94
+ flags `--unmix-spectra` and `--unmix-beads`.
95
+ - **Selecting a sample or gate on large files is fast.** At 20M events,
96
+ clicking a sample row took 1.4 s and clicking a gate row 13 s. Every replot
97
+ shuffled all events to pick the drawn subsample, re-scanned them for
98
+ missing values, and recounted each gate's chain twice.
99
+ - Now 0.2 s for either once a sample has been prepared. The first view of
100
+ a sample takes 0.4 s, and 1.8 s for a gate row.
101
+ - Filter mode went from 8.1 s to 0.18 s, Highlight from 3.0 s to 0.27 s,
102
+ and Histogram from 1.1 s to 0.17 s.
103
+ - After a change, the displayed sample is drawn first. The other loaded
104
+ samples are prepared in the background, at about 1.7 s per 20M sample,
105
+ without blocking the window.
106
+ - No count, percentage or histogram value changes. The drawn subsample is
107
+ still fixed per sample, axes and cap, and now also across restarts.
108
+ - Caches are dropped whenever a sample's data or a gate on its chain
109
+ changes, and are bounded at 192 MB.
110
+ - **Gate bounds are shown as intensities, not stored logicle values.** A cut
111
+ at an intensity of about 1,000 read `CD3-A >= 0.453` and now reads
112
+ `CD3-A >= 987`, in the same style as the axis ticks.
113
+ - Applies to the gate list, unnamed population names in Statistics,
114
+ Frequencies and their CSVs, the histogram slider, the auto-threshold
115
+ status, the backgate legend, the Expression, FMO and Workspace windows,
116
+ and the `openflo-run` log.
117
+ - Open sides are said as open (`CD3-A < 2.5K`) instead of printing the
118
+ ±1e12 sentinel. A channel of unknown scale keeps its stored value,
119
+ marked "(stored, scale unknown)".
120
+ - Colour bars and the CLI scatter PNGs are ticked at round intensities
121
+ (`0, 100, 10^3, 10^4`) instead of `0.2 … 1.0`.
122
+ - Populations are still matched across samples by gate structure, so
123
+ Frequencies is unaffected by the new names.
124
+ - **Frequencies: a population missing from a sample is missing, not 0.**
125
+ - Populations were matched across samples by their displayed path. An
126
+ unnamed gate's path includes its coordinates, so a cut nudged in one
127
+ sample, or a marker on another detector, split one population into
128
+ several.
129
+ - Differential abundance then counted each gap as 0 events. A gate drawn
130
+ only in one group's samples read as a population that had vanished from
131
+ the other group.
132
+ - Populations now match by gate name, or by kind of gate, markers and
133
+ position among siblings.
134
+ - A sample that still lacks a population shows it as missing in the
135
+ table, the tests, the overview and the exports. Differential abundance
136
+ fits it on the samples that have it and reports `n_missing`.
137
+ - %Parent of an empty parent is NaN, not 0%.
138
+ - **Compensation is applied once, with each sample's own matrix.** Reported
139
+ as "either ignored or applied twice"; both happened.
140
+ - **Applied twice.** A second compensation call (`auto_compensate` then
141
+ `compensate_from_wsp`, the same call twice, or `FMOGater.prepare` run
142
+ twice) compensated values that were already compensated: a channel with
143
+ 0.30 spillover moved by about 15,000 units. A new matrix now replaces the
144
+ one applied before. Compensating a channel that is already transformed
145
+ raises `CompensationError` instead of mixing logicle values.
146
+ - **Wrong matrix or none.** `compensate_from_wsp` applied the workspace's
147
+ first matrix to every file, and raised on a workspace with no matrix.
148
+ That affected Compare workspace, `FMOGater` and `Experiment.run_all`. It
149
+ now uses the matrix inside the file's own `<Sample>`, or the file's
150
+ `$SPILL` (FlowJo's Acquisition-defined) when the workspace gives it none.
151
+ Several same-named matrices are no longer collapsed into the last one.
152
+ - **Editor.** Opening a `.wsp` applied each FCS's `$SPILL` instead of the
153
+ workspace's matrix for that sample. A reopened session, a sample moved
154
+ to another window and a workspace re-run silently went back to `$SPILL`,
155
+ discarding a matrix the user had applied. Each now uses the matrix the
156
+ sample was compensated with. A sample restored from a processed CSV keeps
157
+ its matrix and the link to its FCS.
158
+ - **FlowJo gate coordinates are converted to and from OpenFlo's display
159
+ scale.** A `.wsp` holds gates in linear intensity, while the editor and
160
+ `openflo-run` keep gates in the stored (e.g. logicle) coordinates. Nothing
161
+ converted between them:
162
+ - an imported `FL1-A > 1000` selected 0 events where FlowJo counts 7,653;
163
+ - an exported logicle threshold of 0.454 reached FlowJo as an intensity of
164
+ 0.454, so FlowJo counted 14,087 events where OpenFlo counts 7,653.
165
+
166
+ Import (workspace and template) and export (editor and `--export-wsp`) now
167
+ convert per channel. Bounds convert exactly. Polygon edges are subdivided,
168
+ and ellipses are traced as polygons. A channel of unknown scale is left
169
+ unconverted, with a warning. Importing also keeps sibling gates on one
170
+ channel: one workspace's 5 gates arrived as 3, with a child moved under
171
+ the wrong parent.
172
+ - **GeoMean and rCV follow FlowJo's definitions.** These change the
173
+ statistics table and its CSV.
174
+ - rCV is now 100 × ½ (P84.13 − P15.87) / median. The MAD form used before
175
+ read 74% where FlowJo reports 89% on a skewed population.
176
+ - GeoMean on a transformed channel is the mean in display space,
177
+ back-transformed. It used to drop every non-positive event, so a dim
178
+ population read 212 where FlowJo reports 84.
179
+ - **Differential abundance is calibrated.** The negative-binomial test gave
180
+ a false positive in 24% of no-difference 3-vs-3 comparisons at the 5%
181
+ level, and in 64–84% for nested gates. It solved the Poisson score and
182
+ used one dispersion for every population, and nested parent gates pulled
183
+ that dispersion to almost zero. Each population now has its own NB fit
184
+ and dispersion, with a likelihood-ratio test on an F(1, df) reference.
185
+ Simulations give 5–6% false positives, and rows gain a `df` column.
186
+ One sample per group now gives no p-value.
187
+ - **One fold-change direction: log2(B / A) everywhere.** `differential_test`
188
+ alone gave A over B, so one comparison showed opposite signs in two
189
+ windows. A group mean of 0 now gives ±inf, not ±29.2 from a 1e-9
190
+ pseudo-count.
191
+ - **Absolute counts use the bead and sample volumes and the dilution.**
192
+ 50 µL of beads at 1,000/µL in 100 µL of blood read 2,000 cells/µL where
193
+ 1,000 is right. With no volumes entered, the result is unchanged.
194
+ - **`compare_conditions` compares populations, not cluster numbers.**
195
+ Samples clustered one by one number their clusters independently, so
196
+ cluster 0 could be CD4 cells in one sample and CD8 cells in another: a
197
+ CD4 split of 80% vs 20% read 80% vs 80%. The compared samples are now
198
+ clustered together first. `openflo-run` already did this.
199
+ - **Compensation matrix files.**
200
+ - A `$SPILL` that names its channels by parameter number now compensates.
201
+ Before, nothing matched and the sample stayed uncompensated.
202
+ - A matrix in percent (diagonal ≈100) is converted, with a warning, from
203
+ any source. Before, it left every channel at 0.01×.
204
+ - CSV rows are matched to the header by their labels.
205
+ - Header-only CSVs from FlowJo now read.
206
+ - A `.wsp` with no matrix returns `(None, None)`, as documented.
207
+ - **Optimize compensation is unbiased.** It now uses the difference of the
208
+ positive and negative medians and drops saturated events. On bead peaks
209
+ the estimate was 0.158 for a true 0.20, and 0.63 with 3% of events
210
+ saturated; it is now 0.200.
211
+ - **Compensation QC** ranks spillover by magnitude relative to each source's
212
+ own signal. A −0.35 entry ranked below +0.04.
213
+ - **Spectral unmixing.**
214
+ - The editor unmixes what the detectors measured, i.e. the values before
215
+ compensation and transform. It used to read compensated logicle values
216
+ (median abundance error 36–54%).
217
+ - The spillover spread matrix uses the standard formula, net of the
218
+ negative population. It overstated spread 4–6× when the control was
219
+ mostly negative, and understated it at 0.58× with non-negative
220
+ unmixing.
221
+ - `run_qc` drops the same events from `raw`, so `--unmix` no longer
222
+ unmixes events that QC removed.
223
+ - **Values reported on the linear scale.** Several outputs carried logicle
224
+ coordinates as intensities:
225
+ - CLI `<sample>_stats.csv` and `compare_shared_clusters.csv` medians read
226
+ 0.43 / 0.75 where 796 / 20,104 is right;
227
+ - the trajectory trends export;
228
+ - the AnnData export, which keeps X and adds a `linear` layer and each
229
+ marker's transform;
230
+ - the FMO and auto-gate status lines, which now give the linear cut;
231
+ - the heatmaps, which now say they are on the display scale.
232
+ - **GPU logicle** no longer clamps values above ~7.4×10⁵ (error 0.16 →
233
+ 2×10⁻⁸).
234
+ - **Scatter, Time and instrument channels are recognised in any case**
235
+ (`TIME`, `fsc-a`, Beckman `FS INT`/`SS INT`, Sony `BSC-A`, CyTOF
236
+ `Event_length`…). They were logicle-transformed and clustered as markers.
237
+ - **Switching a channel to 'log'** now asks first and names how many events
238
+ at or below zero it will empty.
239
+ - **Gating.**
240
+ - A threshold counts `x >= value`, as Gating-ML and FlowJo do. On integer
241
+ data events exactly on the cut were lost: 50 vs FlowJo's 60. This
242
+ includes FMO `_pos` flags.
243
+ - A gate drawn to the end of an axis is open there, so the negative
244
+ events piled on the axis floor are inside. A rectangle from the corner
245
+ counted 271 of 2,543 events.
246
+ - Axis ticks and the range dialog are in intensity, not stored logicle
247
+ units.
248
+ - A `.wsp` template applies one sample's gate tree, not every sample's.
249
+ - **Compare workspace.**
250
+ - A gate on a bare (uncompensated) parameter is counted on uncompensated
251
+ values, as FlowJo does: 5,216 → 8,413.
252
+ - A population under a gate OpenFlo cannot read is reported as an error.
253
+ It used to be re-rooted and counted.
254
+ - **Cancel stops a Workspace run's whole process tree on Linux and macOS.**
255
+ Before, it stopped only the job process, so PhenoGraph's worker pool and
256
+ the Louvain binaries kept running. Windows already killed the tree.
257
+ - **Events removed by the editor's load QC are reported** in the load status
258
+ and the `.wsp` export warning. FlowJo has no such step, so its counts
259
+ include them.
260
+ - **Ellipses drawn in FlowJo are imported.** FlowJo stores such an ellipse
261
+ as foci and edge points in its display space. The import (workspace,
262
+ template, Compare workspace) now converts it with the sample's own axes:
263
+ exactly on linear axes, and as a 64-vertex polygon on logicle and biex axes
264
+ (FlowJo's biex lookup table). Before, it was dropped and each population
265
+ under it counted every event the ellipse excluded.
266
+ - An ellipse on any other axis (log, arcsinh, a linear gain other than 1)
267
+ becomes a new gate kind, `flowjo_ellipse`. It counts no events, its
268
+ populations stay under it, the gate list and status bar flag it, and an
269
+ export to FlowJo writes it back as FlowJo drew it, with its axes.
270
+ - **Session files:** the `.flowsession` keys and schema version are
271
+ unchanged (v1), and older sessions load as before. A session can now
272
+ contain a `flowjo_ellipse` gate. An older OpenFlo does not know the kind
273
+ and lets every event through it, so open such sessions in this version
274
+ or later.
275
+ - **Single-stain auto-detect** (Optimize compensation). A marker name in the
276
+ channel no longer decides the match (`CD4 FL3-A` took `FL5.fcs`), `-A` and
277
+ `-H` channels of one fluor share its control, and joined names match
278
+ (`AF647`, `CD3FLA`, `eF450`, `compFLA`). A file that two fluors fit
279
+ equally is reported, not assigned.
280
+
281
+ ## [2.6.1] - 2026-10-02
282
+
283
+ ### Fixed
284
+ - **FlowJo round trip, measured against FlowJo 10.10.2.** FlowJo opened,
285
+ computed and saved synthetic probe workspaces and OpenFlo exports with no
286
+ manual steps; each FlowJo behaviour below comes from those saved files.
287
+ - **Ellipse gates.** A workspace with an ellipse gate lost every sample that
288
+ carried it, with no message. Ellipses are now exported as 64-vertex
289
+ polygons.
290
+ - **Quadrants.** A quadrant was exported as a QuadrantGate, which FlowJo
291
+ counts as the whole parent. It is now four rectangle gates in FlowJo's own
292
+ form, with each open side left unbounded, and a child gate stays under its
293
+ own sector. Importing FlowJo's own quadrants also failed: their sectors
294
+ have one bound per side, and the reader skipped every such rectangle.
295
+ - **Fluorescence gates were applied to uncompensated data in FlowJo.** They
296
+ were exported under the raw channel name, which FlowJo reads as the raw
297
+ parameter. They are now written as `Comp-<channel>` for each channel the
298
+ sample's own matrix compensates.
299
+ - **Per-sample `<Keywords>`.** Without the FCS TEXT in each sample, FlowJo
300
+ keeps the sample but computes none of its populations.
301
+ - **Time gates were 100× off** across the `.wsp` boundary. FlowJo works in
302
+ seconds: ticks × the FCS file's own `$TIMESTEP`, or ($ETIM − $BTIM) / last
303
+ tick without one, even when the workspace says otherwise. Export and
304
+ import now convert the same way.
305
+ - **Compensation.** `read_compensation_matrix` found no spillover in any FCS
306
+ file, because flowio lower-cases keywords. It now reads SPILL, and prefers
307
+ it over `$SPILLOVER`, as FlowJo does. Each exported sample carries its own
308
+ matrix, so a run that mixes files with different spillover no longer gets
309
+ one file's matrix applied to all.
310
+ - **Import of a lower edge near the bottom of an axis.** FlowJo treats such
311
+ an edge as open. Imported gates now do the same, so an imported gate no
312
+ longer undercounts by the events below the axis. The writer warns about
313
+ each exported edge FlowJo will open.
314
+ - Polygon counts still differ: by about 2% for scatter polygons and 3-7%
315
+ for exported ellipses. See "Where FlowJo and OpenFlo count differently" in
316
+ the README.
317
+
9
318
  ## [2.6.0] - 2026-09-10
10
319
 
11
320
  ### Added
@@ -503,7 +812,7 @@ filename, and the loader looks for both.
503
812
  40-population workspace arrived fully unnamed while its geometry survived
504
813
  intact, which is why a round trip looked like it worked. A compensation
505
814
  matrix whose *channel name contained a comma* (a `$PnS` description like
506
- "CD4 PE-Cy7, clone SK3" is ordinary) wrote a file this same module then
815
+ "CD4 FL3, clone SK3" is ordinary) wrote a file this same module then
507
816
  refused to read. Standards-conformant Gating-ML `-INF` bounds reached the
508
817
  session file as `-Infinity`, which *RFC 8259 forbids* — valid to Python,
509
818
  rejected by `JSON.parse` and `jq`. An *unusable reference spectrum* (a
@@ -736,7 +1045,7 @@ filename, and the loader looks for both.
736
1045
  - **The golden baseline's compensation metric never ran compensation.** It
737
1046
  wrote the generator's own planted spillover matrix to a CSV, read it straight
738
1047
  back, and asserted the value it had just written — a metric labelled
739
- "Compensation APC -> APC-Fire spill" that could not observe compensation
1048
+ "Compensation FL2 -> FL8 spill" that could not observe compensation
740
1049
  being broken, and which therefore stayed green through the 2.2.1 transpose
741
1050
  bug that corrupted every real compensated analysis. It now applies
742
1051
  compensation to data with a known spillover baked in and reports two numbers
@@ -744,7 +1053,7 @@ filename, and the loader looks for both.
744
1053
  error against the true signal, relative to signal scale) and
745
1054
  `signal_retained` (which a "zero everything" implementation would fail while
746
1055
  flattering the residual). Verified to catch a transposed matrix, a zeroing
747
- implementation, and a no-op. **Baseline keys changed**: `compensation.apc_leak`
1056
+ implementation, and a no-op. **Baseline keys changed**: `compensation.fl2_leak`
748
1057
  is replaced by `compensation.residual_err` + `compensation.signal_retained`.
749
1058
  - **`--update` could bless a bug as the baseline.** Re-pinning rewrites every
750
1059
  golden value from the current run, so running it while a bug was live
@@ -1051,7 +1360,7 @@ filename, and the loader looks for both.
1051
1360
  sample. Malformed or incomplete gate dicts are reported rather than discarded
1052
1361
  in silence.
1053
1362
  - **CLI `--gates` help documented a form the parser rejects.** Copying the
1054
- documented `{"BV421-A":0.5}` example ran the analysis **ungated**; the help now
1363
+ documented `{"FL1-A":0.5}` example ran the analysis **ungated**; the help now
1055
1364
  shows the accepted JSON-list form.
1056
1365
  - **`--export-wsp` resolved zero samples** for a comma-string or per-sample-dict
1057
1366
  `samples` spec (it iterated the string or dict). It now normalises groups
@@ -1190,7 +1499,7 @@ filename, and the loader looks for both.
1190
1499
  - Session restore: a corrupt per-channel display range is skipped instead of
1191
1500
  aborting the whole load; auto-saved sessions are schema-migrated on resume
1192
1501
  (same as a manual open).
1193
- - FlowJo import: gates on natively-compensated channels (FlowJo's `<PE-A>`
1502
+ - FlowJo import: gates on natively-compensated channels (FlowJo's `<FL5-A>`
1194
1503
  bracket form) now match the data instead of selecting everything; the
1195
1504
  region/CLI gate filter evaluates all gate kinds (ellipsoid / cluster /
1196
1505
  category / boolean), not just lines and polygons.
@@ -1819,10 +2128,10 @@ filename, and the loader looks for both.
1819
2128
  cells along a differentiation trajectory: a symmetric kNN graph over the
1820
2129
  enabled samples' shared fluor channels (concatenated, so a day-series becomes
1821
2130
  one continuous trajectory), with pseudotime = geodesic distance from a root
1822
- cell chosen at the extreme of a marker (e.g. CD34-high progenitors as t=0).
2131
+ cell chosen at the extreme of a marker (e.g. CD902-high progenitors as t=0).
1823
2132
  It writes a ``pseudotime`` column to every sample (selectable as a plot
1824
2133
  colour) and draws each marker's mean expression along pseudotime — the
1825
- CD34-down / CD11b-up maturation curve — with CSV / Prism XY / figure export.
2134
+ CD902-down / CD901b-up maturation curve — with CSV / Prism XY / figure export.
1826
2135
  Backend (`openflo.trajectory`: `compute_pseudotime`, `robust_root`,
1827
2136
  `pseudotime_trends`) is pure (numpy/scipy/sklearn), subsamples large data for
1828
2137
  the graph and propagates by nearest neighbour, and is exported.
@@ -1883,7 +2192,7 @@ filename, and the loader looks for both.
1883
2192
  the current plot into a publication-style small-multiples figure: one panel
1884
2193
  per sample (current channels), one panel per channel pair (samples overlaid),
1885
2194
  a samples × pairs grid, or a single panel. Channel pairs accept marker labels
1886
- or channel names (e.g. `CD34/CD11b, CD11b/CD45`). Each panel reuses the live
2195
+ or channel names (e.g. `CD902/CD901b, CD901b/CD903`). Each panel reuses the live
1887
2196
  rendering pipeline (mode, density/colour, axis scales, gates) via an
1888
2197
  axes-swap (`_render_into`), so panels match the on-screen plot exactly. A
1889
2198
  preview window saves to PNG / PDF / SVG / TIFF at 300 dpi, with a
@@ -1918,7 +2227,7 @@ filename, and the loader looks for both.
1918
2227
  reads a CD↔fluorophore sheet and maps each fluorophore to its detector
1919
2228
  channel, merged with `--labels`. New `read_staining_panel` / `find_panel_xlsx`.
1920
2229
  - **Per-group marker-pair scatters (CLI).** Every group now emits, for each
1921
- pair in `--pairs` (default CD34/CD11b, CD11b/CD45, CD34/CD45), an *overlay*
2230
+ pair in `--pairs` (default CD902/CD901b, CD901b/CD903, CD902/CD903), an *overlay*
1922
2231
  (all samples on one axes, coloured by sample) and a *grid* (one density panel
1923
2232
  per sample, shared limits). New `save_group_pair_scatters`.
1924
2233
  - **Adjustable plot point cap.** A **Max points** control (presets + free
@@ -2188,7 +2497,7 @@ First public release.
2188
2497
  stamps each gate's channel with its antibody label; applying that
2189
2498
  template to a sample where the marker sits on a *different* detector
2190
2499
  retargets the gate to that sample's detector (`relabel_gate_for_sample`).
2191
- So a CD11b gate applies wherever CD11b lives in each sample, across
2500
+ So a CD901b gate applies wherever CD901b lives in each sample, across
2192
2501
  panels — compensation is unaffected (only which column the gate
2193
2502
  reads changes).
2194
2503
  - **Per-sample FMO override in the config GUI.** A group's samples
@@ -2206,7 +2515,7 @@ First public release.
2206
2515
  the common label set, renaming each sample's fluors to labels). The
2207
2516
  statistics table now names per-channel columns by **each sample's own**
2208
2517
  label, so a marker on different fluors merges into one column
2209
- (`Median CD11b`) across samples. The editor flags a non-common fluor
2518
+ (`Median CD901b`) across samples. The editor flags a non-common fluor
2210
2519
  panel on sample load and in the Statistics window; non-common labels
2211
2520
  are simply blank where absent.
2212
2521
  - **Population statistics table (FlowJo-style).** A "Statistics…" window
@@ -2214,7 +2523,7 @@ First public release.
2214
2523
  as the cumulative gate chain): Count, %Parent, %Total, and per-channel
2215
2524
  Median / Mean / CV. Columns are modular (checkbox toggles); the table
2216
2525
  exports to analysis-ready CSV. Populations show a FlowJo-style path
2217
- (`Cells/Singlets/CD11b+`). Computed on full sample data, not the plot
2526
+ (`Cells/Singlets/CD901b+`). Computed on full sample data, not the plot
2218
2527
  downsample.
2219
2528
  - **Full FlowJo gate parity — ellipsoid + quadrant.** `WspReader`
2220
2529
  parses `EllipsoidGate` (mean + covariance + distanceSquare) and
@@ -2345,7 +2654,7 @@ First public release.
2345
2654
  for single-stain control files now uses an ordered candidate
2346
2655
  tokenizer (joined form first, then each dash-separated part
2347
2656
  longest-first) instead of the naïve "first dash-separated token"
2348
- heuristic. `PE-Cy7-A` now produces tokens `['pecy7', 'cy7']`
2657
+ heuristic. `FL3-A` now produces tokens `['fl3', 'cy7']`
2349
2658
  instead of just `['pe']`. Ambiguous and unmatched channels surface
2350
2659
  in the status bar.
2351
2660
 
@@ -7,3 +7,6 @@ recursive-include templates *.json *.md
7
7
  recursive-include src/openflo/template_library *.json *.md
8
8
  include CHANGELOG.md
9
9
  include src/openflo/_golden.json
10
+ # The public-mirror publisher lists the private tokens its scrub gate blocks,
11
+ # so it must never ship (same reason it is export-ignore in .gitattributes).
12
+ exclude scripts/publish_public.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openflo
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: Flow cytometry analysis pipeline + Tk GUI with FlowJo .wsp round-trip
5
5
  Author: Skyler Niedzielski
6
6
  License-Expression: MIT
@@ -69,8 +69,8 @@ FlowJo `.wsp` that round-trips back to the desktop tool. A themed (light/dark)
69
69
  editor with resizable, pop-out panels drives it all interactively.
70
70
 
71
71
  > Panel-agnostic — point it at any FCS set and pass `--channels`. The
72
- > examples throughout use a generic 3-colour panel (BV421=CD11b, APC=CD34,
73
- > PE-Cy7=CD45); substitute your own markers and controls.
72
+ > examples throughout use a generic 3-colour panel (FL1=CD901b, FL2=CD902,
73
+ > FL3=CD903); substitute your own markers and controls.
74
74
 
75
75
  > ### 📣 Free to use — the one ask is a citation
76
76
  > OpenFlo is free (MIT). If it contributes to research you **publish or
@@ -266,7 +266,7 @@ openflo-run \
266
266
  --trials trials/day1,trials/day2 \
267
267
  --groups '[{"name":"Day1","samples":["m1","m2"],"fmo_set":"standard"},
268
268
  {"name":"Day2","samples":["m1","m2"],"fmo_set":"standard"}]' \
269
- --fmo-sets '{"standard":{"BV421-A":"bv421-fmo","APC-A":"apc-fmo","PE-Cy7-A":"pecy7-fmo"}}' \
269
+ --fmo-sets '{"standard":{"FL1-A":"fl1-fmo","FL2-A":"fl2-fmo","FL3-A":"fl3-fmo"}}' \
270
270
  --k 30 \
271
271
  --workers 4 \
272
272
  --seed 42 \
@@ -278,6 +278,75 @@ The result: `outputs/Day1/`, `outputs/Day2/`, each with cluster
278
278
  heatmaps + UMAP + per-sample stats; plus a single `analysis.wsp` you
279
279
  can open in FlowJo to validate the gates.
280
280
 
281
+ #### What `openflo-run` writes
282
+
283
+ With several `--trials` folders in the default `independent` mode, each trial
284
+ gets its own sub-folder holding all of the below.
285
+
286
+ - **Per sample**, in `<out>/<group>/`: `<name>_stats.csv` and the sample's
287
+ plots (`<name>_heatmap.png`, `_scatter.png`, `_fsc_ssc.png`).
288
+ `<name>_stats.csv` has one row per cluster of that sample's **own**
289
+ clustering: `cluster`, `population`, `count`, `pct_total` and a
290
+ `median_<marker>` per marker. For every threshold gate applied to the
291
+ sample (its FMO set, or a `--gates` threshold) it also has a
292
+ `pct_pos_<marker>` column: the % of the cluster's events above the cut.
293
+ - **Per group**, in `<out>/`: `<group>_<trial>_umap.png` (`<group>_umap.png`
294
+ with `--batch-mode concatenate`) and the marker-pair scatters
295
+ (`..._<X>_<Y>_overlay.png`, `_grid.png`).
296
+ - **Per pair of groups**, in `<out>/`: `compare_<A>_vs_<B>.csv` and `.png`,
297
+ each cluster's `mean_pct`, `sd_pct` and `n_samples` per group, and a
298
+ `cluster_ids` column saying where the ids come from (below).
299
+ - **`compare_shared_clusters.csv`**, in `<out>/`: what each comparison cluster
300
+ is. One row per sample and cluster: `group`, `sample`, `cluster`,
301
+ `population`, `count`, `pct_total`, `median_<marker>`.
302
+
303
+ **A comparison's cluster ids are not the `<name>_stats.csv` ids.**
304
+ PhenoGraph, Leiden and FlowSOM number clusters arbitrarily, so one id names
305
+ different populations in two separately clustered samples. The comparisons
306
+ therefore come from ONE clustering of every sample of an analysis together
307
+ (one per trial folder in `independent` mode). Read a comparison id's phenotype
308
+ in `compare_shared_clusters.csv`, never in a `<name>_stats.csv`. That shared
309
+ clustering:
310
+
311
+ - clusters at most `--max-events` events **IN TOTAL**, an equal share from
312
+ each sample (a smaller sample gives all it has). With no cap it clusters as
313
+ many events as the largest sample holds, or Leiden's 200,000 / FlowSOM's
314
+ 50,000 when that is smaller. Every other event takes the label of its
315
+ nearest clustered event; with FlowSOM, its best-matching SOM node's.
316
+ - matches channels across samples by antibody label ($PnS, `--labels`,
317
+ `--panel`), so a marker that moved to another fluor on one day still lines
318
+ up. A channel with no label is matched by detector. The run names every
319
+ channel it leaves out or matches across different detectors.
320
+ - is written together with every comparison, or not at all. If any sample of
321
+ any group is missing (it failed, its file was not found, a trial folder of
322
+ a `concatenate` run lacks its file, or `--skip-existing` kept it but its
323
+ file is gone or cannot be reloaded), no comparison file is written, the
324
+ run names each missing sample and why, files from an earlier run are
325
+ left as they were, and the run exits with status 1. The same holds when
326
+ the shared clustering itself fails. A single pair whose comparison fails or has no
327
+ rows gets no file, and its file from an earlier run is removed (the run
328
+ says so), so no old file sits beside the new set. The files are written
329
+ to a new `.compare-staging-*` folder and moved into place together once
330
+ all exist, so a run killed while computing them leaves the earlier set
331
+ untouched. A run killed while the files are moved can leave part of the
332
+ earlier set, part of the new one, or none; the next run replaces it, as
333
+ every run recomputes the comparisons, and removes the staging folder.
334
+ If a comparison file is in use (open in Excel), nothing is
335
+ moved: the earlier set stays, the run names the file and the staging
336
+ folder holding the new set, goes on with any other trial folder, and
337
+ exits with status 1.
338
+ - is recomputed on every run. `--skip-existing` skips only the per-sample
339
+ work: the kept samples' events are reloaded (not re-clustered) and the
340
+ comparisons made again, so they always match the current groups, files,
341
+ settings and OpenFlo version. The comparison files of an earlier run
342
+ (`compare_shared_clusters.csv` and every `compare_<A>_vs_<B>.csv` /
343
+ `.png`) are replaced or removed; other files, such as an
344
+ `openflo-compare` report, are left alone.
345
+
346
+ **GUI, Pipeline Workspace → Batch folder…**: each file's `<label>_clusters.csv`
347
+ and `<label>_events.csv` come from at most the run bar's *max ev* events (a
348
+ random subsample, seeded by the run seed), as a **Run** does.
349
+
281
350
  ### Compare OpenFlo's gating against FlowJo
282
351
 
283
352
  `openflo-compare` reads a FlowJo workspace, re-applies every Population
@@ -294,6 +363,35 @@ The HTML report colour-codes |Δ| > 5% rows for review. Use it to
294
363
  validate a new OpenFlo version against an established FlowJo
295
364
  analysis before switching the analysis pipeline over.
296
365
 
366
+ ### Where FlowJo and OpenFlo count differently
367
+
368
+ Measured against FlowJo 10.10.2, which opened, computed and saved
369
+ OpenFlo-exported workspaces and synthetic probe workspaces:
370
+
371
+ - **Rectangle gates agree exactly.** That covers 1-D ranges, 2-D rectangles
372
+ and quadrant sectors.
373
+ - **Polygon gates do not.** FlowJo decides polygon membership on a display
374
+ grid whose size `gateResolution` sets (default 256). It is not an exact
375
+ point-in-polygon test, and its exact rule is not known. On real data
376
+ FlowJo's polygon populations, and everything gated below them, usually come
377
+ out higher than OpenFlo's: by about 2% for scatter polygons, and by 3-7% for
378
+ exported ellipses. The display axis matters too. With no axis settings in
379
+ the workspace, FlowJo uses each FCS file's `P<n>DISPLAY` keyword (LIN or
380
+ LOG), so the same polygon can count differently on two acquisitions.
381
+ OpenFlo keeps the exact test.
382
+ - **The bottom of an axis.** FlowJo treats a rectangle's lower edge at or near
383
+ the bottom of an axis as open, and also counts every event below the axis.
384
+ On linear scatter and Time axes, "near" means within about 20 units of the
385
+ axis minimum: a Time gate that starts at 10 s starts at 0 s in FlowJo. A
386
+ polygon whose bottom reaches the axis minimum is opened the same way. On
387
+ import (`openflo-compare`, the gate editor, templates) OpenFlo applies the
388
+ rule to rectangles. On export it cannot change FlowJo, so it lists each
389
+ affected gate as a warning.
390
+ - **Ellipse gates** are exported as 64-vertex polygons, with a warning.
391
+ FlowJo silently drops a whole sample that carries a Gating-ML ellipse.
392
+ - **Time gates** cross the `.wsp` boundary in seconds (ticks × `$TIMESTEP`),
393
+ the unit FlowJo uses.
394
+
297
395
  ---
298
396
 
299
397
  ## Synthetic data & self-test