PyLV200 0.1.2__tar.gz → 0.1.3__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 (156) hide show
  1. pylv200-0.1.3/MANIFEST.in +8 -0
  2. {pylv200-0.1.2/src/PyLV200.egg-info → pylv200-0.1.3}/PKG-INFO +8 -2
  3. {pylv200-0.1.2 → pylv200-0.1.3}/README.md +7 -1
  4. pylv200-0.1.3/demo/README.md +13 -0
  5. pylv200-0.1.3/demo/expected_result.json +8 -0
  6. pylv200-0.1.3/demo/open_app.py +46 -0
  7. pylv200-0.1.3/demo/run_demo.py +61 -0
  8. pylv200-0.1.3/docs/software-performance/independent-image-arrays.npz +0 -0
  9. pylv200-0.1.3/docs/software-performance/independent-images.json +41 -0
  10. {pylv200-0.1.2 → pylv200-0.1.3}/pylv200_context.json +8 -8
  11. {pylv200-0.1.2 → pylv200-0.1.3}/pyproject.toml +1 -1
  12. pylv200-0.1.3/scripts/benchmark_accepted_residuals.py +84 -0
  13. pylv200-0.1.3/scripts/benchmark_catalogues.py +73 -0
  14. pylv200-0.1.3/scripts/benchmark_channel_statistics.py +62 -0
  15. pylv200-0.1.3/scripts/benchmark_concurrent.py +52 -0
  16. pylv200-0.1.3/scripts/benchmark_desktop.py +157 -0
  17. pylv200-0.1.3/scripts/benchmark_desktop_preparation.py +43 -0
  18. pylv200-0.1.3/scripts/benchmark_discovery.py +49 -0
  19. pylv200-0.1.3/scripts/benchmark_downsample_residual.py +44 -0
  20. pylv200-0.1.3/scripts/benchmark_export_memory.py +140 -0
  21. pylv200-0.1.3/scripts/benchmark_first_export.py +59 -0
  22. pylv200-0.1.3/scripts/benchmark_kernels.py +106 -0
  23. pylv200-0.1.3/scripts/benchmark_lifecycle.py +123 -0
  24. pylv200-0.1.3/scripts/benchmark_native_preview.py +52 -0
  25. pylv200-0.1.3/scripts/benchmark_parser_residual.py +30 -0
  26. pylv200-0.1.3/scripts/benchmark_processed.py +49 -0
  27. pylv200-0.1.3/scripts/benchmark_proxy.py +44 -0
  28. pylv200-0.1.3/scripts/benchmark_pyramid.py +46 -0
  29. pylv200-0.1.3/scripts/benchmark_remaining_kernels.py +74 -0
  30. pylv200-0.1.3/scripts/benchmark_residuals.py +55 -0
  31. pylv200-0.1.3/scripts/benchmark_setup_enumeration.py +49 -0
  32. pylv200-0.1.3/scripts/benchmark_sidecars.py +14 -0
  33. pylv200-0.1.3/scripts/benchmark_tiled_timeline.py +39 -0
  34. pylv200-0.1.3/scripts/benchmark_unpack.py +60 -0
  35. pylv200-0.1.3/scripts/benchmark_vsi.py +67 -0
  36. pylv200-0.1.3/scripts/benchmark_workflows.py +271 -0
  37. pylv200-0.1.3/scripts/build_release.py +224 -0
  38. pylv200-0.1.3/scripts/check_live_metadata.py +53 -0
  39. pylv200-0.1.3/scripts/check_processing_domain.py +42 -0
  40. pylv200-0.1.3/scripts/check_profiler_scope.py +32 -0
  41. pylv200-0.1.3/scripts/check_speed_completion.py +154 -0
  42. pylv200-0.1.3/scripts/compare_blur_workflow.py +79 -0
  43. pylv200-0.1.3/scripts/compare_desktop_import_overlap.py +84 -0
  44. pylv200-0.1.3/scripts/compare_workflows.py +156 -0
  45. pylv200-0.1.3/scripts/contracts-requirements.txt +4 -0
  46. pylv200-0.1.3/scripts/deploy_candidate.py +34 -0
  47. pylv200-0.1.3/scripts/map_function_costs.py +43 -0
  48. pylv200-0.1.3/scripts/performance_audit.py +185 -0
  49. pylv200-0.1.3/scripts/profile_speed_boundaries.py +57 -0
  50. pylv200-0.1.3/scripts/project_contract.py +400 -0
  51. pylv200-0.1.3/scripts/record_residual_decisions.py +62 -0
  52. pylv200-0.1.3/scripts/refresh_software_alignment.py +50 -0
  53. pylv200-0.1.3/scripts/reindex_speed_profile.py +52 -0
  54. pylv200-0.1.3/scripts/release_preflight.py +266 -0
  55. pylv200-0.1.3/scripts/review_function_costs.py +124 -0
  56. pylv200-0.1.3/scripts/review_speed_regressions.py +71 -0
  57. pylv200-0.1.3/scripts/run_remaining_prototypes.py +38 -0
  58. pylv200-0.1.3/scripts/run_speed_round.py +31 -0
  59. pylv200-0.1.3/scripts/summarize_speed_review.py +68 -0
  60. pylv200-0.1.3/scripts/verify.py +6 -0
  61. pylv200-0.1.3/scripts/verify_image_references.py +92 -0
  62. pylv200-0.1.3/scripts/verify_preview_layout.py +76 -0
  63. pylv200-0.1.3/scripts/verify_release.py +53 -0
  64. pylv200-0.1.3/scripts/verify_speed_candidate.py +52 -0
  65. {pylv200-0.1.2 → pylv200-0.1.3/src/PyLV200.egg-info}/PKG-INFO +8 -2
  66. pylv200-0.1.3/src/PyLV200.egg-info/SOURCES.txt +152 -0
  67. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/__init__.py +45 -29
  68. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/__main__.py +7 -7
  69. pylv200-0.1.3/src/pylv200/_atomic.py +24 -0
  70. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/actions.py +342 -342
  71. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/channels.py +2 -1
  72. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/cli.py +1 -1
  73. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/client.py +3 -2
  74. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/compat.py +83 -83
  75. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/config.py +2 -6
  76. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/context.py +1 -1
  77. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/discover.py +16 -4
  78. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/engine.py +1895 -1798
  79. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/errors.py +107 -107
  80. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/__init__.py +21 -10
  81. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/__main__.py +8 -8
  82. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/app.py +49 -38
  83. pylv200-0.1.3/src/pylv200/gui/embedded.py +65 -0
  84. pylv200-0.1.3/src/pylv200/gui/launcher.py +48 -0
  85. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/timeline.py +567 -567
  86. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/models.py +11 -0
  87. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/oex.py +134 -134
  88. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/options.py +2 -4
  89. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/preview.py +6 -2
  90. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/proxy.py +208 -200
  91. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/pylv200_context.json +8 -8
  92. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/setup.py +29 -8
  93. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/tiffstack.py +453 -453
  94. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/timeline.py +1637 -1630
  95. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/vsi.py +412 -407
  96. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_actions.py +79 -79
  97. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_cli_parity.py +326 -326
  98. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_cli_selection.py +319 -319
  99. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_client.py +215 -215
  100. pylv200-0.1.3/tests/test_desktop_launcher.py +71 -0
  101. pylv200-0.1.3/tests/test_embedded_gui.py +42 -0
  102. pylv200-0.1.3/tests/test_independent_images.py +19 -0
  103. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_manifest.py +306 -306
  104. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_mirror.py +232 -232
  105. pylv200-0.1.3/tests/test_performance_contracts.py +186 -0
  106. pylv200-0.1.3/tests/test_profile_inventory.py +56 -0
  107. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_progress.py +206 -206
  108. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_proxy.py +252 -252
  109. pylv200-0.1.3/tests/test_remaining_performance.py +59 -0
  110. pylv200-0.1.3/tests/test_speed_completion.py +105 -0
  111. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_state.py +214 -214
  112. pylv200-0.1.3/tests/test_streaming_export.py +102 -0
  113. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_tiffstack.py +326 -326
  114. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_timeline_gui.py +174 -174
  115. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_watch.py +317 -317
  116. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_watch_controls.py +42 -42
  117. pylv200-0.1.2/MANIFEST.in +0 -3
  118. pylv200-0.1.2/src/PyLV200.egg-info/SOURCES.txt +0 -82
  119. {pylv200-0.1.2 → pylv200-0.1.3}/LICENSE +0 -0
  120. {pylv200-0.1.2 → pylv200-0.1.3}/README_AI.md +0 -0
  121. {pylv200-0.1.2 → pylv200-0.1.3}/setup.cfg +0 -0
  122. {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/dependency_links.txt +0 -0
  123. {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/entry_points.txt +0 -0
  124. {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/requires.txt +0 -0
  125. {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/top_level.txt +0 -0
  126. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/README_AI.md +0 -0
  127. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/cache.py +0 -0
  128. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/dialogs.py +0 -0
  129. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/preview.py +0 -0
  130. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/protocol.py +0 -0
  131. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/theme.py +0 -0
  132. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/widgets.py +0 -0
  133. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/manifest.py +0 -0
  134. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/protocol.py +0 -0
  135. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/schedule.py +0 -0
  136. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/state.py +0 -0
  137. {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/watch.py +0 -0
  138. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_api.py +0 -0
  139. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_api_parity.py +0 -0
  140. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_cli.py +0 -0
  141. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_config.py +0 -0
  142. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_console.py +0 -0
  143. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_gui.py +0 -0
  144. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_handoff.py +0 -0
  145. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_models.py +0 -0
  146. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_naming.py +0 -0
  147. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_packaging.py +0 -0
  148. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_preview.py +0 -0
  149. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_protocol.py +0 -0
  150. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_readme.py +0 -0
  151. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_roundtrip.py +0 -0
  152. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_schedule.py +0 -0
  153. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_setup.py +0 -0
  154. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_timeline.py +0 -0
  155. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_ui_audit_parity.py +0 -0
  156. {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_vsi.py +0 -0
@@ -0,0 +1,8 @@
1
+ include README_AI.md
2
+ include pylv200_context.json
3
+ recursive-include src/pylv200 README_AI.md pylv200_context.json
4
+
5
+ recursive-include demo *.py *.json *.md
6
+ include docs/software-performance/independent-images.json
7
+ include docs/software-performance/independent-image-arrays.npz
8
+ recursive-include scripts *.py *.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyLV200
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Pull images from a running Olympus/Evident cellSens experiment before the .vsi is written
5
5
  Author: Jamie Malcolm
6
6
  License-Expression: MIT
@@ -94,7 +94,7 @@ probe left`, `list`, `--help`.
94
94
  It is built locally rather than committed, so it is not in a fresh clone. See
95
95
  [docs/BUILDING.md](docs/BUILDING.md).
96
96
 
97
- The desktop application is distributed from the GitHub release page as a
97
+ The desktop application is distributed from the [public Windows downloads page](https://github.com/Jay2owe/PyLV200-downloads/releases/latest) as a
98
98
  portable Windows ZIP and a per-user `PyLV200-<version>-Setup.exe` installer.
99
99
  Both are built and checked against the matching Python package in one release
100
100
  run.
@@ -841,3 +841,9 @@ pytest
841
841
  ## Licence
842
842
 
843
843
  MIT.
844
+
845
+ ## Development review
846
+
847
+ [Performance changes](docs/PERFORMANCE.md) cover dataset discovery, previews and reduced export memory. Timing comparisons use synthetic workloads; live instrument throughput has not been certified.
848
+
849
+ Open the example desktop with `python demo/open_app.py --reload`; run focused checks with `python -m pytest -q tests/test_performance_contracts.py`. The [offline example](demo/README.md) documents isolation and expected-answer controls.
@@ -47,7 +47,7 @@ probe left`, `list`, `--help`.
47
47
  It is built locally rather than committed, so it is not in a fresh clone. See
48
48
  [docs/BUILDING.md](docs/BUILDING.md).
49
49
 
50
- The desktop application is distributed from the GitHub release page as a
50
+ The desktop application is distributed from the [public Windows downloads page](https://github.com/Jay2owe/PyLV200-downloads/releases/latest) as a
51
51
  portable Windows ZIP and a per-user `PyLV200-<version>-Setup.exe` installer.
52
52
  Both are built and checked against the matching Python package in one release
53
53
  run.
@@ -794,3 +794,9 @@ pytest
794
794
  ## Licence
795
795
 
796
796
  MIT.
797
+
798
+ ## Development review
799
+
800
+ [Performance changes](docs/PERFORMANCE.md) cover dataset discovery, previews and reduced export memory. Timing comparisons use synthetic workloads; live instrument throughput has not been certified.
801
+
802
+ Open the example desktop with `python demo/open_app.py --reload`; run focused checks with `python -m pytest -q tests/test_performance_contracts.py`. The [offline example](demo/README.md) documents isolation and expected-answer controls.
@@ -0,0 +1,13 @@
1
+ # Offline acquisition example
2
+
3
+ Status: verified headless; every exported pixel matches the fixed analytic fixture. The image viewer is available with `--show`.
4
+
5
+ From this checkout, run `python demo/run_demo.py`. It generates tiny deterministic input, uses the real public client, exports a TIFF and checks every pixel with zero tolerance. `python demo/run_demo.py --show` opens the actual image viewer. Real sources and settings are isolated; temporary output is removed on exit.
6
+
7
+ [Expected answers](expected_result.json) describe analytic pixel values, not biological measurements. Explicitly approving changed answers requires `python demo/run_demo.py --update-expected --reason "explanation"`; this records the reason and must be reviewed alongside the fixture changes. Ordinary runs never update expected answers.
8
+
9
+ [Verification and performance review](../docs/software-performance/README.md) records the broader app checks. Fixture generators live in tests/; no external acquisition or network connection is needed.
10
+
11
+ ## Desktop development
12
+
13
+ Run `python demo/open_app.py --reload` to open the actual application on isolated synthetic data and restart it after source edits. Closing the window stops the launcher. `python demo/open_app.py --check` opens the populated desktop and closes it automatically for verification. Both populated-desktop check commands passed in the current review.
@@ -0,0 +1,8 @@
1
+ {
2
+ "shape": [3, 2, 8, 8],
3
+ "dtype": "uint16",
4
+ "first_pixel_by_frame_channel": [[1000, 2000], [1010, 2010], [1020, 2020]],
5
+ "pixel_sum": 579840,
6
+ "tolerance": 0,
7
+ "source": "Analytic tile values from tests/synthetic.py: 1000*(channel+1)+10*time. No fitted or implementation-generated expected statistics."
8
+ }
@@ -0,0 +1,46 @@
1
+ """Open the actual desktop on isolated example data; optionally reload on save."""
2
+ import argparse
3
+ import os
4
+ from pathlib import Path
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+
9
+ ROOT=Path(__file__).resolve().parents[1]
10
+
11
+ def desktop(work,reload=False,check=False):
12
+ sys.path[:0]=[str(ROOT/'src'),str(ROOT)]
13
+ os.environ['PYLV200_HOME']=str(work/'settings')
14
+ from tests.synthetic import make_ets,make_vsi
15
+ from pylv200 import Source
16
+ from pylv200.config import save_source
17
+ from pylv200.gui.app import App
18
+ import tkinter as tk
19
+ source=work/'source'
20
+ make_ets(source/'sample.ets',tile=64,nc=2,nt=4,grid_x=1,grid_y=1)
21
+ make_vsi(source/'sample.vsi',['BF','GFP'],experiment='Example')
22
+ save_source(Source(name='Example',roots=(str(source),)))
23
+ window=tk.Tk();app=App(window,target='Example:Example')
24
+ def signature():return {str(p):p.stat().st_mtime_ns for p in (ROOT/'src/pylv200').rglob('*.py')}
25
+ original=signature();changed=[]
26
+ def tick():
27
+ if reload and signature()!=original:
28
+ changed.append(True);app.on_close();return
29
+ if check and app.tree.get_children() and not app.busy:
30
+ app.on_close();return
31
+ window.after(500,tick)
32
+ window.after(500,tick);window.mainloop()
33
+ return 75 if changed else 0
34
+
35
+ if __name__=='__main__':
36
+ p=argparse.ArgumentParser(description=__doc__)
37
+ p.add_argument('--reload',action='store_true');p.add_argument('--check',action='store_true')
38
+ p.add_argument('--work',type=Path,help=argparse.SUPPRESS);a=p.parse_args()
39
+ if a.work:raise SystemExit(desktop(a.work,a.reload,a.check))
40
+ with tempfile.TemporaryDirectory(prefix='pylv200-desktop-demo-') as folder:
41
+ command=[sys.executable,__file__,'--work',folder]
42
+ if a.reload:command.append('--reload')
43
+ if a.check:command.append('--check')
44
+ while True:
45
+ done=subprocess.run(command)
46
+ if done.returncode!=75:raise SystemExit(done.returncode)
@@ -0,0 +1,61 @@
1
+ """Pull a tiny synthetic acquisition and verify every exported pixel offline."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import tempfile
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path[:0] = [str(ROOT / "src"), str(ROOT)]
12
+
13
+
14
+ def run(directory, show=False, update_reason=None):
15
+ os.environ["PYLV200_HOME"] = str(directory / "settings")
16
+ import numpy as np
17
+ import tifffile
18
+ from pylv200 import LV200Client
19
+ from tests.synthetic import make_ets, make_vsi
20
+ source = directory / "source"
21
+ make_ets(source / "sample.ets", tile=8, nc=2, nt=3, grid_x=1, grid_y=1)
22
+ make_vsi(source / "sample.vsi", ["BF", "GFP"], experiment="Synthetic demo")
23
+ with LV200Client(source) as client:
24
+ result = client.fetch("Synthetic demo", output=str(directory / "output"), include_last=True)
25
+ assert result.files, result.summary()
26
+ pixels = tifffile.imread(result.files[0].path).reshape(3, 2, 8, 8)
27
+ expected_path = Path(__file__).with_name("expected_result.json")
28
+ expected = json.loads(expected_path.read_text())
29
+ if update_reason:
30
+ from datetime import datetime, timezone
31
+ expected.update(shape=list(pixels.shape), dtype=str(pixels.dtype),
32
+ first_pixel_by_frame_channel=pixels[:, :, 0, 0].tolist(),
33
+ pixel_sum=int(pixels.sum()), tolerance=0)
34
+ expected.setdefault("updates", []).append(dict(reason=update_reason,
35
+ at=datetime.now(timezone.utc).isoformat()))
36
+ from pylv200._atomic import write_json
37
+ write_json(expected_path, expected)
38
+ expected_pixels = np.broadcast_to(np.asarray(expected["first_pixel_by_frame_channel"],
39
+ dtype=np.uint16)[:, :, None, None], pixels.shape)
40
+ np.testing.assert_array_equal(pixels, expected_pixels)
41
+ assert list(pixels.shape) == expected["shape"]
42
+ assert str(pixels.dtype) == expected["dtype"]
43
+ assert int(pixels.sum()) == expected["pixel_sum"]
44
+ if show:
45
+ client.preview("Synthetic demo").show(title="PyLV200 — synthetic demo")
46
+ return dict(verified=True, frames=3, channels=2, pixels=int(pixels.size), tolerance=0)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ parser = argparse.ArgumentParser(description=__doc__)
51
+ parser.add_argument("--show", action="store_true", help="Open the real preview viewer after verification")
52
+ parser.add_argument("--update-expected", action="store_true",
53
+ help="Explicitly approve new expected pixels; requires --reason")
54
+ parser.add_argument("--reason", help="Recorded reason for approving changed fixture answers")
55
+ args = parser.parse_args()
56
+ if args.update_expected and not (args.reason or "").strip():
57
+ parser.error("--update-expected requires a nonempty --reason")
58
+ if args.reason and not args.update_expected:
59
+ parser.error("--reason is only valid with --update-expected")
60
+ with tempfile.TemporaryDirectory(prefix="pylv200-demo-") as folder:
61
+ print(json.dumps(run(Path(folder), args.show, args.reason if args.update_expected else None)))
@@ -0,0 +1,41 @@
1
+ {
2
+ "rows": [
3
+ {
4
+ "capability": "PNG pixel encoding",
5
+ "case": "(1, 1)",
6
+ "reference": "Pillow 12.3.0",
7
+ "scope": "Independent decoder",
8
+ "status": "exact",
9
+ "max_error": 0,
10
+ "tolerance": 0
11
+ },
12
+ {
13
+ "capability": "PNG pixel encoding",
14
+ "case": "(7, 13)",
15
+ "reference": "Pillow 12.3.0",
16
+ "scope": "Independent decoder",
17
+ "status": "exact",
18
+ "max_error": 0,
19
+ "tolerance": 0
20
+ },
21
+ {
22
+ "capability": "PNG pixel encoding",
23
+ "case": "(64, 64)",
24
+ "reference": "Pillow 12.3.0",
25
+ "scope": "Independent decoder",
26
+ "status": "exact",
27
+ "max_error": 0,
28
+ "tolerance": 0
29
+ },
30
+ {
31
+ "capability": "Complete exported TIFF pixels",
32
+ "case": "Synthetic_demo.tif",
33
+ "reference": "Pillow 12.3.0",
34
+ "scope": "Independent pixel decoder; proprietary input metadata not certified",
35
+ "status": "exact",
36
+ "max_error": 0,
37
+ "tolerance": 0
38
+ }
39
+ ],
40
+ "arrays_sha256": "32d73544bc8387f6a6e492e7d918fcc15985cc7b2d369121b1e4bb411bbb2033"
41
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "pylv200",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "reader_contract": {
5
5
  "python": "from pylv200 import context; context.read(topic=None, format='text')",
6
6
  "search": "context.search(query, limit=5)",
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "ok": true,
13
13
  "package": "pylv200",
14
- "version": "0.1.2",
14
+ "version": "0.1.3",
15
15
  "topic": "inspect",
16
16
  "title": "Identify an acquisition safely",
17
17
  "description": "Check roots and resolve the exact experiment before planning or pulling.",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "ok": true,
29
29
  "package": "pylv200",
30
- "version": "0.1.2",
30
+ "version": "0.1.3",
31
31
  "topic": "overview",
32
32
  "title": "PyLV200 overview",
33
33
  "description": "Read live cellSens LV200 acquisitions from filesystem roots and pull compatible image stacks.",
@@ -42,7 +42,7 @@
42
42
  {
43
43
  "ok": true,
44
44
  "package": "pylv200",
45
- "version": "0.1.2",
45
+ "version": "0.1.3",
46
46
  "topic": "plan-and-pull",
47
47
  "title": "Plan and pull image stacks",
48
48
  "description": "Preview the files and time window before writing a resumable pull.",
@@ -58,7 +58,7 @@
58
58
  {
59
59
  "ok": true,
60
60
  "package": "pylv200",
61
- "version": "0.1.2",
61
+ "version": "0.1.3",
62
62
  "topic": "preview",
63
63
  "title": "Preview a live acquisition",
64
64
  "description": "Read a bounded set of tiles to identify positions and channels before pulling.",
@@ -74,7 +74,7 @@
74
74
  {
75
75
  "ok": true,
76
76
  "package": "pylv200",
77
- "version": "0.1.2",
77
+ "version": "0.1.3",
78
78
  "topic": "protocol",
79
79
  "title": "Read the acquisition protocol",
80
80
  "description": "Inspect channels, exposures, positions, requested cadence, achieved cadence, and progress.",
@@ -90,7 +90,7 @@
90
90
  {
91
91
  "ok": true,
92
92
  "package": "pylv200",
93
- "version": "0.1.2",
93
+ "version": "0.1.3",
94
94
  "topic": "troubleshooting",
95
95
  "title": "PyLV200 troubleshooting",
96
96
  "description": "Recognise common source, identity, live-file, and output problems.",
@@ -107,7 +107,7 @@
107
107
  {
108
108
  "ok": true,
109
109
  "package": "pylv200",
110
- "version": "0.1.2",
110
+ "version": "0.1.3",
111
111
  "topic": "watch-once",
112
112
  "title": "Collect new data once",
113
113
  "description": "Run one resumable poll for a scheduler or repeated agent call without leaving a worker running.",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PyLV200"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Pull images from a running Olympus/Evident cellSens experiment before the .vsi is written"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,84 @@
1
+ """Reproduce accepted residual changes against immutable original functions."""
2
+ import argparse
3
+ import ast
4
+ import hashlib
5
+ import importlib
6
+ import json
7
+ from pathlib import Path
8
+ import statistics
9
+ import sys
10
+ import time
11
+ import zipfile
12
+
13
+
14
+ def run(root,work):
15
+ sys.path[:0]=[str(root/'src'),str(root)]
16
+ import numpy as np
17
+ import psutil
18
+ from benchmark_workflows import fingerprint
19
+ while psutil.virtual_memory().available<4.25*1024**3:time.sleep(15)
20
+ lv=(root/'src/pylv200').exists();name='pylv200' if lv else 'pyincucyte';prefix='src/pylv200' if lv else name
21
+ archive=root/'docs/software-performance/baseline-source.zip';rows=[]
22
+ def original(module,function):
23
+ current=importlib.import_module(name+'.'+module)
24
+ with zipfile.ZipFile(archive) as z:tree=ast.parse(z.read(prefix+'/'+module+'.py').decode('utf-8-sig'))
25
+ node=next(n for n in tree.body if isinstance(n,ast.FunctionDef) and n.name==function)
26
+ namespace=dict(current.__dict__)
27
+ exec(compile(ast.Module(body=[node],type_ignores=[]),'<immutable original>','exec'),namespace)
28
+ return namespace[function],getattr(current,function)
29
+ def paired(ident,before,after,pairs=9):
30
+ wall={'before':[],'after':[]};cpu={'before':[],'after':[]};host=[]
31
+ for pair in range(pairs):
32
+ results={};psutil.cpu_percent()
33
+ for label in ('before','after') if pair%2==0 else ('after','before'):
34
+ start=time.perf_counter();start_cpu=time.process_time()
35
+ results[label]=(before if label=='before' else after)()
36
+ cpu[label].append(time.process_time()-start_cpu);wall[label].append(time.perf_counter()-start)
37
+ host.append(psutil.cpu_percent())
38
+ if isinstance(results['before'],np.ndarray):np.testing.assert_array_equal(results['before'],results['after'])
39
+ else:assert results['before']==results['after'],ident
40
+ rows.append(dict(id=ident,before_seconds=wall['before'],after_seconds=wall['after'],cpu=cpu,host_cpu_percent=host,
41
+ parity=True,ratio=statistics.median(wall['before'])/statistics.median(wall['after'])))
42
+ before,after=original('models','json_clean')
43
+ for count in (8,64,512):
44
+ data={'files':[{'path':Path('output')/f'field-{i}.tif','frames':96,'channels':[0,1],
45
+ 'shape':[96,2,512,512],'size':100663296,'scale':2.5,'missing':None,
46
+ 'complete':True,'record':{'index':i,'value':float(i)}} for i in range(count)]}
47
+ paired(f'complete_json/{count}',lambda:json.dumps(before(data),allow_nan=False),lambda:json.dumps(after(data),allow_nan=False))
48
+ if lv:
49
+ before,after=original('timeline','_block_mean_proxy')
50
+ data=np.random.default_rng(14).integers(0,65536,(512,512),dtype=np.uint16)
51
+ paired('block_mean_uint16/512',lambda:before(data),lambda:after(data))
52
+ from unittest.mock import patch
53
+ from pylv200 import channels
54
+ before,after=original('channels','channel_stats')
55
+ work.mkdir(parents=True,exist_ok=True)
56
+ for edge in (64,256,1024):
57
+ data=np.random.default_rng(14).integers(0,65536,(10,edge,edge),dtype=np.uint16)
58
+ path=work/'statistics.raw';path.write_bytes(data.tobytes())
59
+ length=edge*edge*2
60
+ meta=dict(chunks=[((i//5,i%5),i*length,length,path) for i in range(10)])
61
+ decoder=lambda blob,_:np.frombuffer(blob,dtype=np.uint16).reshape(edge,edge)
62
+ before.__globals__['decode_tile']=decoder
63
+ try:
64
+ with patch.object(channels,'decode_tile',decoder):
65
+ paired('channel_statistics/'+str(edge),lambda:before(meta,{'names':['c','t']}),lambda:after(meta,{'names':['c','t']}))
66
+ finally:path.unlink()
67
+ before,after=original('setup','candidate_networks')
68
+ paired('setup:network_choices',before,after,pairs=5)
69
+ else:
70
+ before,after=original('timeline','downsample_native')
71
+ for shape,edge in (((512,512),256),((1152,1536),512),((2304,3072),512)):
72
+ data=np.random.default_rng(14).integers(0,65536,shape,dtype=np.uint16)
73
+ paired('downsample/'+str(shape),lambda:before(data,max_edge=edge),lambda:after(data,max_edge=edge))
74
+ return dict(source=fingerprint(root,root/prefix),baseline_sha256=hashlib.sha256(archive.read_bytes()).hexdigest(),
75
+ collector_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),rows=rows,
76
+ limitation='Paired local operations on shared host; full timeline/action collections remain required. Process CPU excludes child PowerShell CPU. Returned network metadata is compared but never persisted.')
77
+
78
+
79
+ if __name__=='__main__':
80
+ parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--root',type=Path,required=True)
81
+ parser.add_argument('--work',type=Path,required=True)
82
+ a=parser.parse_args();root=a.root.resolve();result=run(root,a.work.resolve())
83
+ (root/'docs/software-performance/accepted-residuals.json').write_text(json.dumps(result,indent=2)+'\n')
84
+ print([(r['id'],r['ratio']) for r in result['rows']])
@@ -0,0 +1,73 @@
1
+ """Paired real-client catalogue growth probes, separate from image-size trials."""
2
+ import argparse
3
+ import contextlib
4
+ import importlib
5
+ import io
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import sys
10
+ import struct
11
+ import time
12
+ from benchmark_workflows import canonical, environment, fingerprint, memory
13
+
14
+
15
+ def run(root,work,size,thumbnail_bytes=0):
16
+ lv=(root/'src/pylv200').exists();name='pylv200' if lv else 'pyincucyte'
17
+ sys.path[:0]=[str(root/'src'),str(root),str(root/'tests')]
18
+ work.mkdir(parents=True,exist_ok=True)
19
+ os.environ['PYLV200_HOME']=str(work/'settings');os.environ['PYINCUCYTE_HOME']=str(work/'settings')
20
+ package=root/('src/pylv200' if lv else 'pyincucyte');source=fingerprint(root,package)
21
+ pkg=importlib.import_module(name);count={'small':8,'typical':64,'growth':512}[size];rows=[]
22
+ with contextlib.ExitStack() as stack:
23
+ if lv:
24
+ from tests.synthetic import make_ets,make_vsi
25
+ for i in range(count):
26
+ folder=work/'input'/f'Run{i:04}'
27
+ make_ets(folder/'sample.ets',tile=16,nc=2,nt=2,grid_x=1,grid_y=1)
28
+ make_vsi(folder/'sample.vsi',['BF','GFP'],experiment=f'Run{i:04}')
29
+ if thumbnail_bytes:
30
+ import random
31
+ file=folder/'sample.vsi';blob=bytearray(file.read_bytes())
32
+ ifd=struct.unpack_from('<I',blob,4)[0]
33
+ count_tags=struct.unpack_from('<H',blob,ifd)[0]
34
+ for index in range(count_tags):
35
+ entry=ifd+2+12*index
36
+ if struct.unpack_from('<H',blob,entry)[0]==33560:
37
+ offset=struct.unpack_from('<I',blob,entry+8)[0]
38
+ assert blob[offset:offset+4]==b'SIS0'
39
+ struct.pack_into('<I',blob,entry+8,offset+thumbnail_bytes)
40
+ prefix=random.Random(20261014).randbytes(thumbnail_bytes)
41
+ file.write_bytes(blob[:offset]+prefix+blob[offset:])
42
+ break
43
+ else:raise AssertionError('Missing declared metadata offset')
44
+ for p in folder.iterdir():os.utime(p,(1770000000,1770000000))
45
+ client=stack.enter_context(pkg.LV200Client(work/'input'))
46
+ fresh=lambda:client.experiments(refresh=True,live=True)
47
+ cached=lambda:client.experiments()
48
+ else:
49
+ from fakes import FakeDevice,patched,logged_in_store,vessel_record
50
+ device=FakeDevice(vessels=[vessel_record(vessel_id=i+1,name=f'Run{i:04}') for i in range(count)])
51
+ stack.enter_context(patched(device))
52
+ client=stack.enter_context(pkg.IncucyteClient('10.0.0.1',store=logged_in_store(work)))
53
+ fresh=lambda:client.vessels(refresh=True)
54
+ cached=lambda:client.vessels()
55
+ outputs={}
56
+ for label,fn in [('catalogue:fresh',fresh),('catalogue:warm',cached),('catalogue:refresh',fresh)]:
57
+ if thumbnail_bytes:label=label.replace('catalogue:','catalogue:thumbnail_')
58
+ start=time.perf_counter();value=fn();elapsed=time.perf_counter()-start
59
+ assert len(value)==count
60
+ result=canonical([v.to_dict() for v in value],work)
61
+ rows.append(dict(id=label,seconds=elapsed,result=result,memory=memory()))
62
+ outputs[label]=result
63
+ assert source==fingerprint(root,package)
64
+ return dict(package=name,size=size,source=source,rows=rows,outputs=outputs,environment=environment(),fixture={'catalogue_items':count,'opaque_thumbnail_sized_prefix_bytes':thumbnail_bytes},limitations='Actual client methods; synthetic files or simulated device transport. Optional opaque prefix represents thumbnail byte-scan cost, not JPEG decoding.')
65
+
66
+
67
+ if __name__=='__main__':
68
+ p=argparse.ArgumentParser(description=__doc__)
69
+ for name in ('root','work','out'):p.add_argument('--'+name,type=Path,required=True)
70
+ p.add_argument('--size',choices=('small','typical','growth'),default='small');a=p.parse_args()
71
+ with contextlib.redirect_stdout(io.StringIO()):result=run(a.root.resolve(),a.work.resolve(),a.size)
72
+ a.out.write_text(json.dumps(result,indent=2,default=str)+'\n')
73
+ print(json.dumps({'ok':True,'cases':len(result['rows'])}))
@@ -0,0 +1,62 @@
1
+ """Test repeated variance elimination in complete channel statistics, without editing production."""
2
+ import argparse
3
+ import inspect
4
+ import json
5
+ from pathlib import Path
6
+ import statistics
7
+ import sys
8
+ import time
9
+ from collections import defaultdict
10
+
11
+
12
+ def run(root, work):
13
+ sys.path.insert(0, str(root/'src'))
14
+ import numpy as np
15
+ from pylv200 import channels
16
+ from benchmark_workflows import fingerprint
17
+ work.mkdir(parents=True, exist_ok=True)
18
+ source = inspect.getsource(channels.channel_stats)
19
+ source = source.replace('dx = np.diff(f0, axis=1)', 'dx = np.diff(f0, axis=1)\n dx_variance = dx.var()')
20
+ source = source.replace('f0.var() / dx.var()) if dx.var() > 0', 'f0.var() / dx_variance) if dx_variance > 0')
21
+ namespace = dict(channels.__dict__)
22
+ exec(compile(source, '<single variance prototype>', 'exec'), namespace)
23
+ candidate = namespace['channel_stats']
24
+ rows = []
25
+ for edge in (64, 256, 1024):
26
+ frames = np.random.default_rng(14).integers(0, 65536, (10, edge, edge), dtype=np.uint16)
27
+ path = work/'channel-frames.raw'
28
+ path.write_bytes(frames.tobytes())
29
+ frame_bytes = edge*edge*2
30
+ meta = dict(chunks=[((i//5, i%5), i*frame_bytes, frame_bytes, path) for i in range(10)])
31
+ # Exercise complete file reads and statistics with a fixed raw decoder.
32
+ decoder = lambda blob, _: np.frombuffer(blob, dtype=np.uint16).reshape(edge, edge)
33
+ old_decoder = channels.decode_tile
34
+ channels.decode_tile = decoder
35
+ namespace['decode_tile'] = decoder
36
+ try:
37
+ samples = {'before': [], 'after': []}
38
+ results = {}
39
+ for pair in range(9):
40
+ for label in ('before', 'after') if pair%2 == 0 else ('after', 'before'):
41
+ fn = channels.channel_stats if label == 'before' else candidate
42
+ start = time.perf_counter()
43
+ results[label] = fn(meta, {'names': ['c','t']})
44
+ samples[label].append(time.perf_counter()-start)
45
+ assert results['before'] == results['after']
46
+ rows.append(dict(edge=edge, **samples, parity=True,
47
+ ratio=statistics.median(samples['before'])/statistics.median(samples['after']),
48
+ saved_seconds=statistics.median(samples['before'])-statistics.median(samples['after'])))
49
+ finally:
50
+ channels.decode_tile = old_decoder
51
+ path.unlink()
52
+ return dict(source=fingerprint(root, root/'src/pylv200'), rows=rows,
53
+ scope='Complete statistics on two channels, five raw frames each. Only duplicate horizontal-difference variance removed; production unchanged.')
54
+
55
+
56
+ if __name__ == '__main__':
57
+ p=argparse.ArgumentParser(description=__doc__)
58
+ for name in ('root','work','out'):p.add_argument('--'+name,type=Path,required=True)
59
+ a=p.parse_args()
60
+ result=run(a.root.resolve(),a.work.resolve())
61
+ a.out.write_text(json.dumps(result,indent=2)+'\n')
62
+ print([(r['edge'],r['ratio'],r['saved_seconds']) for r in result['rows']])
@@ -0,0 +1,52 @@
1
+ """Complete PyIncucyte export with its default four-worker download pool."""
2
+ import argparse
3
+ import contextlib
4
+ from datetime import datetime,timedelta
5
+ import io
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import sys
10
+ import time
11
+ from benchmark_workflows import canonical,environment,fingerprint,memory,output_files
12
+
13
+ def run(root,work,size):
14
+ sys.path[:0]=[str(root),str(root/'tests')]
15
+ work.mkdir(parents=True,exist_ok=True);os.environ['PYINCUCYTE_HOME']=str(work/'settings')
16
+ import pyincucyte as pkg
17
+ from fakes import FakeDevice,patched,logged_in_store,tiff_bytes
18
+ before=fingerprint(root,root/'pyincucyte')
19
+ count,edge={'small':(4,64),'typical':(24,256),'growth':(96,512),'native':(24,1536)}[size]
20
+ shape=(1152,1536) if size=='native' else (edge,edge)
21
+ class Device(FakeDevice):
22
+ def fetch_image(self,host,token,item,max_retries=3):
23
+ return pixels[int(item.get('img_type',1))],None
24
+ scans=[(datetime(2026,3,1,9)+timedelta(minutes=5*i)).isoformat() for i in range(count)]
25
+ device=Device(scans=scans,wells=((0,0),(0,1)),channels=(1,2))
26
+ pixels={c:tiff_bytes(value=c*100,shape=shape) for c in (1,2)}
27
+ options=pkg.ExportOptions(output=str(work/'output'),vessels=[38],wells='A1-A2',channels='phase,green',start_from='2026-03-01',end_at='2026-03-01',layout='time_channel_stack')
28
+ assert options.workers==4
29
+ with patched(device),pkg.IncucyteClient('10.0.0.1',store=logged_in_store(work)) as client:
30
+ import psutil
31
+ process=psutil.Process();io_before=process.io_counters();cpu=time.process_time()
32
+ start=time.perf_counter();result=client.fetch(options);elapsed=time.perf_counter()-start
33
+ cpu=time.process_time()-cpu;io_after=process.io_counters()
34
+ assert result.files
35
+ outputs=output_files(work/'output');assert outputs
36
+ native=canonical(result.to_dict(),work)
37
+ comparison=dict(native)
38
+ # The public client appends from the completion callback. Baseline trials
39
+ # themselves return A1/A2 in either order. Keep that exact arrival list in
40
+ # the receipt, and compare independent output files by their declared path.
41
+ files={item['path']:item for item in native['files']}
42
+ assert len(files)==len(native['files'])
43
+ comparison['files']=files
44
+ assert before==fingerprint(root,root/'pyincucyte')
45
+ return dict(package='pyincucyte',size=size,source=before,rows=[dict(id='workflow:default_workers',seconds=elapsed,cpu_seconds=cpu,io_delta={k:getattr(io_after,k)-getattr(io_before,k) for k in io_before._fields},result=comparison,memory=memory())],native_result=native,outputs=outputs,environment=environment(),fixture=dict(frames=count,shape=shape,workers=4),limitations='Real public fetch and worker pool; simulated transport excludes network waits. Native completion order is retained; independent files compared by path. Within-file frame, channel and pixel order are exact. CPU covers all process threads; I/O counters surround the timed fetch.')
46
+
47
+ if __name__=='__main__':
48
+ p=argparse.ArgumentParser(description=__doc__)
49
+ for name in ('root','work','out'):p.add_argument('--'+name,type=Path,required=True)
50
+ p.add_argument('--size',default='small');a=p.parse_args()
51
+ with contextlib.redirect_stdout(io.StringIO()):result=run(a.root.resolve(),a.work.resolve(),a.size)
52
+ a.out.write_text(json.dumps(result,indent=2)+'\n');print('Default-worker export complete')