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.
- pylv200-0.1.3/MANIFEST.in +8 -0
- {pylv200-0.1.2/src/PyLV200.egg-info → pylv200-0.1.3}/PKG-INFO +8 -2
- {pylv200-0.1.2 → pylv200-0.1.3}/README.md +7 -1
- pylv200-0.1.3/demo/README.md +13 -0
- pylv200-0.1.3/demo/expected_result.json +8 -0
- pylv200-0.1.3/demo/open_app.py +46 -0
- pylv200-0.1.3/demo/run_demo.py +61 -0
- pylv200-0.1.3/docs/software-performance/independent-image-arrays.npz +0 -0
- pylv200-0.1.3/docs/software-performance/independent-images.json +41 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/pylv200_context.json +8 -8
- {pylv200-0.1.2 → pylv200-0.1.3}/pyproject.toml +1 -1
- pylv200-0.1.3/scripts/benchmark_accepted_residuals.py +84 -0
- pylv200-0.1.3/scripts/benchmark_catalogues.py +73 -0
- pylv200-0.1.3/scripts/benchmark_channel_statistics.py +62 -0
- pylv200-0.1.3/scripts/benchmark_concurrent.py +52 -0
- pylv200-0.1.3/scripts/benchmark_desktop.py +157 -0
- pylv200-0.1.3/scripts/benchmark_desktop_preparation.py +43 -0
- pylv200-0.1.3/scripts/benchmark_discovery.py +49 -0
- pylv200-0.1.3/scripts/benchmark_downsample_residual.py +44 -0
- pylv200-0.1.3/scripts/benchmark_export_memory.py +140 -0
- pylv200-0.1.3/scripts/benchmark_first_export.py +59 -0
- pylv200-0.1.3/scripts/benchmark_kernels.py +106 -0
- pylv200-0.1.3/scripts/benchmark_lifecycle.py +123 -0
- pylv200-0.1.3/scripts/benchmark_native_preview.py +52 -0
- pylv200-0.1.3/scripts/benchmark_parser_residual.py +30 -0
- pylv200-0.1.3/scripts/benchmark_processed.py +49 -0
- pylv200-0.1.3/scripts/benchmark_proxy.py +44 -0
- pylv200-0.1.3/scripts/benchmark_pyramid.py +46 -0
- pylv200-0.1.3/scripts/benchmark_remaining_kernels.py +74 -0
- pylv200-0.1.3/scripts/benchmark_residuals.py +55 -0
- pylv200-0.1.3/scripts/benchmark_setup_enumeration.py +49 -0
- pylv200-0.1.3/scripts/benchmark_sidecars.py +14 -0
- pylv200-0.1.3/scripts/benchmark_tiled_timeline.py +39 -0
- pylv200-0.1.3/scripts/benchmark_unpack.py +60 -0
- pylv200-0.1.3/scripts/benchmark_vsi.py +67 -0
- pylv200-0.1.3/scripts/benchmark_workflows.py +271 -0
- pylv200-0.1.3/scripts/build_release.py +224 -0
- pylv200-0.1.3/scripts/check_live_metadata.py +53 -0
- pylv200-0.1.3/scripts/check_processing_domain.py +42 -0
- pylv200-0.1.3/scripts/check_profiler_scope.py +32 -0
- pylv200-0.1.3/scripts/check_speed_completion.py +154 -0
- pylv200-0.1.3/scripts/compare_blur_workflow.py +79 -0
- pylv200-0.1.3/scripts/compare_desktop_import_overlap.py +84 -0
- pylv200-0.1.3/scripts/compare_workflows.py +156 -0
- pylv200-0.1.3/scripts/contracts-requirements.txt +4 -0
- pylv200-0.1.3/scripts/deploy_candidate.py +34 -0
- pylv200-0.1.3/scripts/map_function_costs.py +43 -0
- pylv200-0.1.3/scripts/performance_audit.py +185 -0
- pylv200-0.1.3/scripts/profile_speed_boundaries.py +57 -0
- pylv200-0.1.3/scripts/project_contract.py +400 -0
- pylv200-0.1.3/scripts/record_residual_decisions.py +62 -0
- pylv200-0.1.3/scripts/refresh_software_alignment.py +50 -0
- pylv200-0.1.3/scripts/reindex_speed_profile.py +52 -0
- pylv200-0.1.3/scripts/release_preflight.py +266 -0
- pylv200-0.1.3/scripts/review_function_costs.py +124 -0
- pylv200-0.1.3/scripts/review_speed_regressions.py +71 -0
- pylv200-0.1.3/scripts/run_remaining_prototypes.py +38 -0
- pylv200-0.1.3/scripts/run_speed_round.py +31 -0
- pylv200-0.1.3/scripts/summarize_speed_review.py +68 -0
- pylv200-0.1.3/scripts/verify.py +6 -0
- pylv200-0.1.3/scripts/verify_image_references.py +92 -0
- pylv200-0.1.3/scripts/verify_preview_layout.py +76 -0
- pylv200-0.1.3/scripts/verify_release.py +53 -0
- pylv200-0.1.3/scripts/verify_speed_candidate.py +52 -0
- {pylv200-0.1.2 → pylv200-0.1.3/src/PyLV200.egg-info}/PKG-INFO +8 -2
- pylv200-0.1.3/src/PyLV200.egg-info/SOURCES.txt +152 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/__init__.py +45 -29
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/__main__.py +7 -7
- pylv200-0.1.3/src/pylv200/_atomic.py +24 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/actions.py +342 -342
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/channels.py +2 -1
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/cli.py +1 -1
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/client.py +3 -2
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/compat.py +83 -83
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/config.py +2 -6
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/context.py +1 -1
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/discover.py +16 -4
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/engine.py +1895 -1798
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/errors.py +107 -107
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/__init__.py +21 -10
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/__main__.py +8 -8
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/app.py +49 -38
- pylv200-0.1.3/src/pylv200/gui/embedded.py +65 -0
- pylv200-0.1.3/src/pylv200/gui/launcher.py +48 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/timeline.py +567 -567
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/models.py +11 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/oex.py +134 -134
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/options.py +2 -4
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/preview.py +6 -2
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/proxy.py +208 -200
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/pylv200_context.json +8 -8
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/setup.py +29 -8
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/tiffstack.py +453 -453
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/timeline.py +1637 -1630
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/vsi.py +412 -407
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_actions.py +79 -79
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_cli_parity.py +326 -326
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_cli_selection.py +319 -319
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_client.py +215 -215
- pylv200-0.1.3/tests/test_desktop_launcher.py +71 -0
- pylv200-0.1.3/tests/test_embedded_gui.py +42 -0
- pylv200-0.1.3/tests/test_independent_images.py +19 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_manifest.py +306 -306
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_mirror.py +232 -232
- pylv200-0.1.3/tests/test_performance_contracts.py +186 -0
- pylv200-0.1.3/tests/test_profile_inventory.py +56 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_progress.py +206 -206
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_proxy.py +252 -252
- pylv200-0.1.3/tests/test_remaining_performance.py +59 -0
- pylv200-0.1.3/tests/test_speed_completion.py +105 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_state.py +214 -214
- pylv200-0.1.3/tests/test_streaming_export.py +102 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_tiffstack.py +326 -326
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_timeline_gui.py +174 -174
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_watch.py +317 -317
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_watch_controls.py +42 -42
- pylv200-0.1.2/MANIFEST.in +0 -3
- pylv200-0.1.2/src/PyLV200.egg-info/SOURCES.txt +0 -82
- {pylv200-0.1.2 → pylv200-0.1.3}/LICENSE +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/README_AI.md +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/setup.cfg +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/dependency_links.txt +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/entry_points.txt +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/requires.txt +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/PyLV200.egg-info/top_level.txt +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/README_AI.md +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/cache.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/dialogs.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/preview.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/protocol.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/theme.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/gui/widgets.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/manifest.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/protocol.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/schedule.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/state.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/src/pylv200/watch.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_api.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_api_parity.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_cli.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_config.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_console.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_gui.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_handoff.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_models.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_naming.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_packaging.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_preview.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_protocol.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_readme.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_roundtrip.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_schedule.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_setup.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_timeline.py +0 -0
- {pylv200-0.1.2 → pylv200-0.1.3}/tests/test_ui_audit_parity.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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)))
|
|
Binary file
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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')
|