mesc-io 0.1.0__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 (38) hide show
  1. mesc_io-0.1.0/LICENSE +21 -0
  2. mesc_io-0.1.0/PKG-INFO +125 -0
  3. mesc_io-0.1.0/README.md +87 -0
  4. mesc_io-0.1.0/pyproject.toml +52 -0
  5. mesc_io-0.1.0/setup.cfg +4 -0
  6. mesc_io-0.1.0/src/mesc_io/__init__.py +8 -0
  7. mesc_io-0.1.0/src/mesc_io/__main__.py +214 -0
  8. mesc_io-0.1.0/src/mesc_io/check.py +143 -0
  9. mesc_io-0.1.0/src/mesc_io/dff.py +238 -0
  10. mesc_io-0.1.0/src/mesc_io/errors.py +17 -0
  11. mesc_io-0.1.0/src/mesc_io/export.py +81 -0
  12. mesc_io-0.1.0/src/mesc_io/frames_io.py +78 -0
  13. mesc_io-0.1.0/src/mesc_io/reader.py +303 -0
  14. mesc_io-0.1.0/src/mesc_io/registration.py +246 -0
  15. mesc_io-0.1.0/src/mesc_io/values.py +62 -0
  16. mesc_io-0.1.0/src/mesc_io/viewer.html +1001 -0
  17. mesc_io-0.1.0/src/mesc_io/viewer.py +729 -0
  18. mesc_io-0.1.0/src/mesc_io/writeback.py +179 -0
  19. mesc_io-0.1.0/src/mesc_io.egg-info/PKG-INFO +125 -0
  20. mesc_io-0.1.0/src/mesc_io.egg-info/SOURCES.txt +36 -0
  21. mesc_io-0.1.0/src/mesc_io.egg-info/dependency_links.txt +1 -0
  22. mesc_io-0.1.0/src/mesc_io.egg-info/entry_points.txt +2 -0
  23. mesc_io-0.1.0/src/mesc_io.egg-info/requires.txt +15 -0
  24. mesc_io-0.1.0/src/mesc_io.egg-info/top_level.txt +1 -0
  25. mesc_io-0.1.0/tests/test_check.py +42 -0
  26. mesc_io-0.1.0/tests/test_cli.py +60 -0
  27. mesc_io-0.1.0/tests/test_dff.py +177 -0
  28. mesc_io-0.1.0/tests/test_export.py +41 -0
  29. mesc_io-0.1.0/tests/test_multi_session.py +53 -0
  30. mesc_io-0.1.0/tests/test_reader.py +67 -0
  31. mesc_io-0.1.0/tests/test_registration.py +90 -0
  32. mesc_io-0.1.0/tests/test_roundtrip_cli.py +73 -0
  33. mesc_io-0.1.0/tests/test_streaming.py +42 -0
  34. mesc_io-0.1.0/tests/test_values.py +38 -0
  35. mesc_io-0.1.0/tests/test_viewer_geometry.py +65 -0
  36. mesc_io-0.1.0/tests/test_viewer_page.py +139 -0
  37. mesc_io-0.1.0/tests/test_viewer_server.py +344 -0
  38. mesc_io-0.1.0/tests/test_writeback.py +100 -0
mesc_io-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the mesc-io authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mesc_io-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: mesc-io
3
+ Version: 0.1.0
4
+ Summary: Read Femtonics .mesc two-photon recordings, in the units the native reader shows
5
+ Author: Lynn Smith
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/LynnYuSmith/mesc-io
8
+ Project-URL: Repository, https://github.com/LynnYuSmith/mesc-io
9
+ Project-URL: Changelog, https://github.com/LynnYuSmith/mesc-io/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/LynnYuSmith/mesc-io/issues
11
+ Keywords: mesc,femtonics,two-photon,calcium-imaging,microscopy,hdf5
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
22
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: h5py>=3.6
27
+ Requires-Dist: numpy>=1.21
28
+ Provides-Extra: tiff
29
+ Requires-Dist: tifffile>=2021.11; extra == "tiff"
30
+ Provides-Extra: imagej
31
+ Requires-Dist: roifile>=2022.9; extra == "imagej"
32
+ Provides-Extra: register
33
+ Requires-Dist: suite2p<1.0,>=0.14; extra == "register"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7; extra == "dev"
36
+ Requires-Dist: roifile>=2022.9; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # mesc-io
40
+
41
+ [![tests](https://github.com/LynnYuSmith/mesc-io/actions/workflows/tests.yml/badge.svg)](https://github.com/LynnYuSmith/mesc-io/actions/workflows/tests.yml)
42
+
43
+ Read Femtonics `.mesc` two-photon recordings in Python — the frames in the units the native
44
+ reader shows, every unit's metadata, an export, a write-back, and a viewer that draws ROIs and
45
+ computes their dF/F.
46
+
47
+ ![the viewer on a synthetic recording: four spots, three of them blinking, their dF/F below](https://raw.githubusercontent.com/LynnYuSmith/mesc-io/main/docs/viewer.png)
48
+
49
+ ## Install
50
+
51
+ Not on PyPI yet. From the repository:
52
+
53
+ ```
54
+ pip install "mesc-io @ git+https://github.com/LynnYuSmith/mesc-io"
55
+ pip install "mesc-io[tiff,imagej] @ git+https://github.com/LynnYuSmith/mesc-io" # + TIFF export, ImageJ ROI export
56
+ ```
57
+
58
+ Needs Python 3.9 or newer, numpy and h5py; nothing else for reading and viewing. Motion
59
+ correction (`mesc-io register`) is an extra, `[register]`, and pulls in Suite2p 0.14.
60
+
61
+ ## Python
62
+
63
+ ```python
64
+ from mesc_io import MescFile
65
+
66
+ with MescFile("recording.mesc") as f:
67
+ for u in f.units():
68
+ print(u.path, u.shape, u.frame_rate_hz, u.pixel_size_um)
69
+
70
+ frames = f.read("MUnit_0", channel=0) # reader units, float64
71
+ frames = f.read("MUnit_0", reader_units=False) # the stored integers
72
+ for block in f.iter_frames("MUnit_0", block=500): # bigger than memory
73
+ ...
74
+ ```
75
+
76
+ `read` refuses over 4 GB by default (`max_gb=None` to override). A unit name that matches in
77
+ more than one session is refused, not resolved to the first; `MSession_1/MUnit_0` always works.
78
+
79
+ ## Command line
80
+
81
+ ```
82
+ mesc-io info recording.mesc # units, rates, pixel sizes
83
+ mesc-io check recording.mesc # what the file disagrees with itself about
84
+ mesc-io export recording.mesc MUnit_0 u0.h5 # or u0.tif
85
+ mesc-io writeback recording.mesc out.mesc MUnit_0 u0.h5 # processed frames back in
86
+ mesc-io view recording.mesc # the recording in a browser
87
+ ```
88
+
89
+ `info` and `check` take `--json`.
90
+
91
+ ### The viewer
92
+
93
+ `mesc-io view` opens the recording in a private browser window: units on the left with a
94
+ thumbnail each and the unit's own metadata (rate, pixel size, the stage position and the
95
+ position from the zero set on the rig); the image in
96
+ the middle — scrub it, average a window of N frames, zoom into a place by dragging a box;
97
+ ROIs on the right (spot, rectangle, polygon; drag to move, type a size); every ROI's time
98
+ course along the bottom, raw or dF/F by our pipeline's method (`mesc_io.dff`, numpy only,
99
+ held against the pipeline's functions in the tests), stacked or overlaid, zoomed by dragging a box. ROIs belong to the
100
+ unit they were drawn on and are saved as you go, beside the working directory, never beside
101
+ the raw file; so is the view itself, so reopening the file puts you back where you were.
102
+ Layout and decisions: [docs/viewer_design.md](docs/viewer_design.md).
103
+
104
+ To try it without a recording, make one:
105
+
106
+ ```
107
+ python tools/make_synthetic.py # writes synthetic_view.mesc: 3 units, 4 spots, 3 of them blinking
108
+ mesc-io view synthetic_view.mesc
109
+ ```
110
+
111
+ ## Development
112
+
113
+ ```
114
+ pip install -e ".[tiff,dev]"
115
+ pytest
116
+ ```
117
+
118
+ The package lives under `src/`, so tests run against the installed copy. CI runs the suite on
119
+ Python 3.9, 3.12 and 3.13 on Linux, macOS and Windows, plus one job with Suite2p for the
120
+ registration tests; the browser checks (`tests/*_real_chrome.js`) run in a real Chrome and are
121
+ not part of pytest.
122
+
123
+ ## Licence
124
+
125
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,87 @@
1
+ # mesc-io
2
+
3
+ [![tests](https://github.com/LynnYuSmith/mesc-io/actions/workflows/tests.yml/badge.svg)](https://github.com/LynnYuSmith/mesc-io/actions/workflows/tests.yml)
4
+
5
+ Read Femtonics `.mesc` two-photon recordings in Python — the frames in the units the native
6
+ reader shows, every unit's metadata, an export, a write-back, and a viewer that draws ROIs and
7
+ computes their dF/F.
8
+
9
+ ![the viewer on a synthetic recording: four spots, three of them blinking, their dF/F below](https://raw.githubusercontent.com/LynnYuSmith/mesc-io/main/docs/viewer.png)
10
+
11
+ ## Install
12
+
13
+ Not on PyPI yet. From the repository:
14
+
15
+ ```
16
+ pip install "mesc-io @ git+https://github.com/LynnYuSmith/mesc-io"
17
+ pip install "mesc-io[tiff,imagej] @ git+https://github.com/LynnYuSmith/mesc-io" # + TIFF export, ImageJ ROI export
18
+ ```
19
+
20
+ Needs Python 3.9 or newer, numpy and h5py; nothing else for reading and viewing. Motion
21
+ correction (`mesc-io register`) is an extra, `[register]`, and pulls in Suite2p 0.14.
22
+
23
+ ## Python
24
+
25
+ ```python
26
+ from mesc_io import MescFile
27
+
28
+ with MescFile("recording.mesc") as f:
29
+ for u in f.units():
30
+ print(u.path, u.shape, u.frame_rate_hz, u.pixel_size_um)
31
+
32
+ frames = f.read("MUnit_0", channel=0) # reader units, float64
33
+ frames = f.read("MUnit_0", reader_units=False) # the stored integers
34
+ for block in f.iter_frames("MUnit_0", block=500): # bigger than memory
35
+ ...
36
+ ```
37
+
38
+ `read` refuses over 4 GB by default (`max_gb=None` to override). A unit name that matches in
39
+ more than one session is refused, not resolved to the first; `MSession_1/MUnit_0` always works.
40
+
41
+ ## Command line
42
+
43
+ ```
44
+ mesc-io info recording.mesc # units, rates, pixel sizes
45
+ mesc-io check recording.mesc # what the file disagrees with itself about
46
+ mesc-io export recording.mesc MUnit_0 u0.h5 # or u0.tif
47
+ mesc-io writeback recording.mesc out.mesc MUnit_0 u0.h5 # processed frames back in
48
+ mesc-io view recording.mesc # the recording in a browser
49
+ ```
50
+
51
+ `info` and `check` take `--json`.
52
+
53
+ ### The viewer
54
+
55
+ `mesc-io view` opens the recording in a private browser window: units on the left with a
56
+ thumbnail each and the unit's own metadata (rate, pixel size, the stage position and the
57
+ position from the zero set on the rig); the image in
58
+ the middle — scrub it, average a window of N frames, zoom into a place by dragging a box;
59
+ ROIs on the right (spot, rectangle, polygon; drag to move, type a size); every ROI's time
60
+ course along the bottom, raw or dF/F by our pipeline's method (`mesc_io.dff`, numpy only,
61
+ held against the pipeline's functions in the tests), stacked or overlaid, zoomed by dragging a box. ROIs belong to the
62
+ unit they were drawn on and are saved as you go, beside the working directory, never beside
63
+ the raw file; so is the view itself, so reopening the file puts you back where you were.
64
+ Layout and decisions: [docs/viewer_design.md](docs/viewer_design.md).
65
+
66
+ To try it without a recording, make one:
67
+
68
+ ```
69
+ python tools/make_synthetic.py # writes synthetic_view.mesc: 3 units, 4 spots, 3 of them blinking
70
+ mesc-io view synthetic_view.mesc
71
+ ```
72
+
73
+ ## Development
74
+
75
+ ```
76
+ pip install -e ".[tiff,dev]"
77
+ pytest
78
+ ```
79
+
80
+ The package lives under `src/`, so tests run against the installed copy. CI runs the suite on
81
+ Python 3.9, 3.12 and 3.13 on Linux, macOS and Windows, plus one job with Suite2p for the
82
+ registration tests; the browser checks (`tests/*_real_chrome.js`) run in a real Chrome and are
83
+ not part of pytest.
84
+
85
+ ## Licence
86
+
87
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mesc-io"
7
+ version = "0.1.0"
8
+ description = "Read Femtonics .mesc two-photon recordings, in the units the native reader shows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Lynn Smith" }]
14
+ keywords = ["mesc", "femtonics", "two-photon", "calcium-imaging", "microscopy", "hdf5"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
26
+ "Topic :: Scientific/Engineering :: Image Processing",
27
+ ]
28
+ dependencies = ["h5py>=3.6", "numpy>=1.21"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/LynnYuSmith/mesc-io"
32
+ Repository = "https://github.com/LynnYuSmith/mesc-io"
33
+ Changelog = "https://github.com/LynnYuSmith/mesc-io/blob/main/CHANGELOG.md"
34
+ Issues = "https://github.com/LynnYuSmith/mesc-io/issues"
35
+
36
+ [project.optional-dependencies]
37
+ tiff = ["tifffile>=2021.11"]
38
+ imagej = ["roifile>=2022.9"]
39
+ register = ["suite2p>=0.14,<1.0"] # 1.x changed compute_reference/register_frames; validated on 0.14.x
40
+ dev = ["pytest>=7", "roifile>=2022.9"]
41
+
42
+ [project.scripts]
43
+ mesc-io = "mesc_io.__main__:main"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ # The viewer's page ships inside the package. Without this it is missing from the wheel and
49
+ # `mesc-io view` fails at the first request with a file-not-found — on someone else's machine,
50
+ # not on the one it was built on.
51
+ [tool.setuptools.package-data]
52
+ mesc_io = ["viewer.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """Read Femtonics `.mesc` two-photon recordings, in the units the native reader shows."""
2
+ from .errors import MescIOError
3
+ from .reader import Channel, MescError, MescFile, Unit
4
+ from .values import ConversionError, from_reader_units, to_reader_units
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["MescFile", "Unit", "Channel", "MescError", "MescIOError",
8
+ "to_reader_units", "from_reader_units", "ConversionError", "__version__"]
@@ -0,0 +1,214 @@
1
+ """`mesc-io` on the command line: look inside a .mesc, get frames out, put frames back."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+ from .errors import MescIOError
10
+ from .reader import MescFile
11
+
12
+
13
+ def _as_json(payload) -> int:
14
+ import json
15
+ print(json.dumps(payload, indent=2, sort_keys=True))
16
+ return 0
17
+
18
+
19
+ def _info(args) -> int:
20
+ with MescFile(args.file) as f:
21
+ units = f.units()
22
+ if args.json:
23
+ return _as_json([{"unit": u.path, "frames": u.n_frames,
24
+ "height": u.height, "width": u.width, "dtype": u.dtype,
25
+ "frame_rate_hz": u.frame_rate_hz,
26
+ "duration_s": u.duration_s,
27
+ "pixel_size_um": u.pixel_size_um,
28
+ "comment": u.comment,
29
+ "channels": [{"name": c.name, "offset": c.offset,
30
+ "scale": c.scale} for c in u.channels]}
31
+ for u in units])
32
+ print(f"{Path(args.file).name}: {len(units)} unit(s)")
33
+ print(f"{'unit':<18}{'frames':>8}{'size':>12}{'rate':>11}{'pixel':>11} comment")
34
+ for u in units:
35
+ rate = f"{u.frame_rate_hz:.3f} Hz" if u.frame_rate_hz else "unknown"
36
+ px = f"{u.pixel_size_um:.4f} um" if u.pixel_size_um else "unknown"
37
+ print(f" {u.path:<16}{u.n_frames:>8}{f'{u.height}x{u.width}':>12}"
38
+ f"{rate:>11}{px:>11} {u.comment[:44]}")
39
+ return 0
40
+
41
+
42
+ def _check(args) -> int:
43
+ from .check import check
44
+ rep = check(args.file)
45
+ if args.json:
46
+ return _as_json({"file": rep.path, "units": rep.n_units, "ok": rep.ok,
47
+ "findings": [{"level": f.level, "code": f.code,
48
+ "message": f.message, "units": list(f.units)}
49
+ for f in rep.findings]})
50
+ print(rep)
51
+ if rep.ok:
52
+ print(" nothing that would mislead an analysis")
53
+ return 0
54
+
55
+
56
+ def _export(args) -> int:
57
+ from . import export as _ex
58
+ with MescFile(args.file) as f:
59
+ out = Path(args.out)
60
+ writer = _ex.to_tiff if out.suffix.lower() in (".tif", ".tiff") else _ex.to_hdf5
61
+ written = writer(f, args.unit, out, channel=args.channel,
62
+ reader_units=not args.stored_units)
63
+ print(f" {written} ({'stored' if args.stored_units else 'reader'} units)")
64
+ return 0
65
+
66
+
67
+ def _writeback(args) -> int:
68
+ from .frames_io import load_frames
69
+ from .writeback import write_frames
70
+
71
+ frames, in_file = load_frames(args.frames, dataset=args.dataset)
72
+ if args.reader_units:
73
+ units = True
74
+ elif args.stored_units:
75
+ units = False
76
+ elif in_file is not None:
77
+ units = in_file
78
+ print(f" {Path(args.frames).name} says its values are in "
79
+ f"{'reader' if units else 'stored'} units")
80
+ else:
81
+ print(" the frames do not say which units they are in — pass --reader-units or "
82
+ "--stored-units. Getting this wrong writes a file that looks right and is not.",
83
+ file=sys.stderr)
84
+ return 1
85
+
86
+ report = write_frames(args.source, args.out, {args.unit: {f"Channel_{args.channel}": frames}},
87
+ reader_units=units, tag=None if args.no_tag else args.tag,
88
+ tolerance=args.tolerance)
89
+ for w in report["warnings"]:
90
+ print(f" note: {w}")
91
+ for what, delta in report["agreement"].items():
92
+ print(f" {what}: agrees with the frames it replaced to {delta:+.0f} counts")
93
+ print(f" {report['out']}")
94
+ return 0
95
+
96
+
97
+ def _view(args) -> int:
98
+ from .viewer import serve
99
+
100
+ serve(args.source, port=args.port, open_browser=not args.no_browser,
101
+ rois_path=args.rois)
102
+ return 0
103
+
104
+
105
+ def _register(args) -> int:
106
+ from .registration import register_file
107
+
108
+ def _say(path, info):
109
+ print(f" {path}: {info['n_frames']} frames, {info['n_channels']} channel(s), "
110
+ f"{info['leading_flat_frames']} flat at the front, "
111
+ f"shift |y|<={abs(info['y_shift']).max()} |x|<={abs(info['x_shift']).max()} px")
112
+
113
+ rep = register_file(args.source, args.out, units=args.units or None, channel=args.channel,
114
+ reference_from=args.reference_from, nonrigid=args.nonrigid,
115
+ block_size=args.block_size, max_shift=args.max_shift,
116
+ max_shift_nr=args.max_shift_nr,
117
+ tag=None if args.no_tag else args.tag, progress=_say)
118
+ print(f" reference from {rep['reference_from']}"
119
+ f"{' (non-rigid)' if rep['nonrigid'] else ''}")
120
+ print(f" {rep['out']}")
121
+ return 0
122
+
123
+
124
+ def main(argv=None) -> int:
125
+ ap = argparse.ArgumentParser(prog="mesc-io", description=__doc__)
126
+ ap.add_argument("--version", action="version", version=f"mesc-io {__version__}")
127
+ sub = ap.add_subparsers(dest="cmd", required=True)
128
+
129
+ p = sub.add_parser("info", help="list the units in a .mesc and what they say about themselves")
130
+ p.add_argument("file")
131
+ p.add_argument("--json", action="store_true", help="machine-readable output")
132
+ p.set_defaults(func=_info)
133
+
134
+ p = sub.add_parser("check", help="report what in a .mesc disagrees with itself")
135
+ p.add_argument("file")
136
+ p.add_argument("--json", action="store_true", help="machine-readable output")
137
+ p.set_defaults(func=_check)
138
+
139
+ p = sub.add_parser("export", help="write one unit to .h5 or .tif (by the output's suffix)")
140
+ p.add_argument("file")
141
+ p.add_argument("unit", help="MUnit_0, or MSession_0/MUnit_0")
142
+ p.add_argument("out")
143
+ p.add_argument("--channel", type=int, default=0)
144
+ p.add_argument("--stored-units", action="store_true",
145
+ help="write the file's own integers instead of the values the reader shows")
146
+ p.set_defaults(func=_export)
147
+
148
+ p = sub.add_parser("writeback",
149
+ help="put processed frames back into a copy of a .mesc")
150
+ p.add_argument("source", help="the original .mesc — it is copied, never modified")
151
+ p.add_argument("out", help="the copy to write")
152
+ p.add_argument("unit", help="MUnit_0, or MSession_0/MUnit_0")
153
+ p.add_argument("frames", help=".h5 or .tif holding the processed frames")
154
+ p.add_argument("--channel", type=int, default=0)
155
+ p.add_argument("--dataset", default=None,
156
+ help="which dataset in the .h5 (only needed if there is more than one)")
157
+ g = p.add_mutually_exclusive_group()
158
+ g.add_argument("--reader-units", action="store_true",
159
+ help="the frames hold the values the reader displays")
160
+ g.add_argument("--stored-units", action="store_true",
161
+ help="the frames hold the file's own integers")
162
+ p.add_argument("--tag", default="_MC",
163
+ help="appended to the written unit's comment (default: _MC)")
164
+ p.add_argument("--no-tag", action="store_true", help="do not mark the written unit")
165
+ p.add_argument("--tolerance", type=float, default=60.0,
166
+ help="how far, in stored counts, the new frames may differ from the ones "
167
+ "they replace before the write is refused (default: 60)")
168
+ p.set_defaults(func=_writeback)
169
+
170
+ p = sub.add_parser("view",
171
+ help="open the recording in a browser: the movie, the file's own "
172
+ "numbers, and traces of regions you draw")
173
+ p.add_argument("source")
174
+ p.add_argument("--port", type=int, default=8020)
175
+ p.add_argument("--no-browser", action="store_true",
176
+ help="do not open a browser; print the address and wait")
177
+ p.add_argument("--rois", default=None,
178
+ help="where the drawn regions are kept (default: beside the CWD, never "
179
+ "beside the raw file — raw data is not ours to add files to)")
180
+ p.set_defaults(func=_view)
181
+
182
+ p = sub.add_parser("register",
183
+ help="motion-correct units against one shared reference (needs Suite2p)")
184
+ p.add_argument("source")
185
+ p.add_argument("out")
186
+ p.add_argument("--units", nargs="*", default=None,
187
+ help="which units; all of them by default. Name them when the file holds "
188
+ "more than one field — one reference across two fields is meaningless")
189
+ p.add_argument("--channel", type=int, default=0,
190
+ help="the channel registration is computed on; the others take its shifts")
191
+ p.add_argument("--reference-from", default=None,
192
+ help="anchor unit (default: the first one), so the reference is the state "
193
+ "the session started in rather than an average over it")
194
+ p.add_argument("--nonrigid", action="store_true",
195
+ help="also correct a smooth position-dependent warp")
196
+ p.add_argument("--block-size", type=int, default=128)
197
+ p.add_argument("--max-shift", type=float, default=0.1,
198
+ help="rigid cap, as a fraction of the frame (default: 0.1)")
199
+ p.add_argument("--max-shift-nr", type=float, default=5.0,
200
+ help="non-rigid cap in px; keep it small (default: 5)")
201
+ p.add_argument("--tag", default="_MC")
202
+ p.add_argument("--no-tag", action="store_true")
203
+ p.set_defaults(func=_register)
204
+
205
+ args = ap.parse_args(argv)
206
+ try:
207
+ return args.func(args)
208
+ except (MescIOError, FileNotFoundError, KeyError, ImportError, MemoryError) as exc:
209
+ print(f" {type(exc).__name__}: {exc}", file=sys.stderr)
210
+ return 1
211
+
212
+
213
+ if __name__ == "__main__":
214
+ raise SystemExit(main())
@@ -0,0 +1,143 @@
1
+ """Look a `.mesc` over before you trust it.
2
+
3
+ A file can be perfectly readable and still not be what you assume. These are the
4
+ disagreements worth knowing about before an analysis is built on top of one — each is
5
+ something a file has actually done, not a hypothetical:
6
+
7
+ * **units recorded at different frame rates.** Normal in a session where the field or the
8
+ zoom changed, and ruinous if you take one rate for the whole file. A z-stack saved
9
+ alongside the recordings reports a frame period that is not a frame rate at all.
10
+ * **units at different pixel sizes.** The zoom was changed between recordings, so distances
11
+ in one unit are not distances in another.
12
+ * **conversion attributes missing.** Then the values you read are the stored integers and the
13
+ package has nothing to convert them with — it says so rather than assuming a common offset.
14
+ * **channels with different conversions across units**, which means a per-file constant is
15
+ the wrong model for that file.
16
+ * **empty or single-frame units**, which are usually a snapshot or an aborted recording and
17
+ will surprise anything that expects a movie.
18
+
19
+ `check` reports; it never repairs. What is a problem depends on what you are doing, so the
20
+ severities are advisory and the report names the units so you can look.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from collections import Counter, defaultdict
25
+ from dataclasses import dataclass, field
26
+ from typing import Dict, List
27
+
28
+ from .reader import MescFile
29
+
30
+ __all__ = ["check", "Report", "Finding"]
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Finding:
35
+ level: str # "note" | "warn"
36
+ code: str
37
+ message: str
38
+ units: tuple = ()
39
+
40
+ def __str__(self) -> str:
41
+ where = f" [{', '.join(self.units[:6])}{'…' if len(self.units) > 6 else ''}]" if self.units else ""
42
+ return f"{self.level:>4}: {self.message}{where}"
43
+
44
+
45
+ @dataclass
46
+ class Report:
47
+ path: str
48
+ n_units: int
49
+ findings: List[Finding] = field(default_factory=list)
50
+
51
+ @property
52
+ def ok(self) -> bool:
53
+ return not any(f.level == "warn" for f in self.findings)
54
+
55
+ def __str__(self) -> str:
56
+ head = f"{self.path}: {self.n_units} unit(s), {len(self.findings)} finding(s)"
57
+ return "\n".join([head] + [str(f) for f in self.findings])
58
+
59
+
60
+ def _group(values: Dict[str, object]) -> Dict[object, List[str]]:
61
+ out = defaultdict(list)
62
+ for unit, v in values.items():
63
+ out[v].append(unit)
64
+ return dict(out)
65
+
66
+
67
+ def check(path) -> Report:
68
+ """Read a `.mesc`'s metadata and report what disagrees with what."""
69
+ with MescFile(path) as f:
70
+ units = f.units()
71
+ rep = Report(path=str(f.path.name), n_units=len(units))
72
+
73
+ rates = {u.path: (round(u.frame_rate_hz, 3) if u.frame_rate_hz else None) for u in units}
74
+ missing = [u for u, r in rates.items() if r is None]
75
+ if missing:
76
+ rep.findings.append(Finding(
77
+ "warn", "no-frame-rate",
78
+ f"{len(missing)} unit(s) do not state a frame rate", tuple(missing)))
79
+ present = {u: r for u, r in rates.items() if r is not None}
80
+ by_rate = _group(present)
81
+ if len(by_rate) > 1:
82
+ common = Counter(present.values()).most_common(1)[0][0]
83
+ odd = tuple(u for u, r in present.items() if r != common)
84
+ rep.findings.append(Finding(
85
+ "warn", "mixed-frame-rates",
86
+ f"{len(by_rate)} different frame rates in one file "
87
+ f"({', '.join(f'{r:g} Hz x{len(v)}' for r, v in sorted(by_rate.items()))}) — "
88
+ f"use each unit's own rate, never one for the file", odd))
89
+
90
+ sizes = {u.path: u.pixel_size_um for u in units if u.pixel_size_um}
91
+ by_size = _group(sizes)
92
+ if len(by_size) > 1:
93
+ rep.findings.append(Finding(
94
+ "warn", "mixed-pixel-sizes",
95
+ f"{len(by_size)} different pixel sizes "
96
+ f"({', '.join(f'{s:.4f} um x{len(v)}' for s, v in sorted(by_size.items()))}) — "
97
+ "distances are not comparable across these units", ()))
98
+ if len(sizes) < len(units):
99
+ rep.findings.append(Finding(
100
+ "note", "no-pixel-size",
101
+ f"{len(units) - len(sizes)} unit(s) do not state a pixel size",
102
+ tuple(u.path for u in units if not u.pixel_size_um)))
103
+
104
+ shapes = _group({u.path: (u.height, u.width) for u in units})
105
+ if len(shapes) > 1:
106
+ rep.findings.append(Finding(
107
+ "note", "mixed-frame-shapes",
108
+ f"{len(shapes)} different frame shapes "
109
+ f"({', '.join(f'{h}x{w}' for h, w in sorted(shapes))})", ()))
110
+
111
+ thin = tuple(u.path for u in units if u.n_frames <= 1)
112
+ if thin:
113
+ rep.findings.append(Finding(
114
+ "note", "not-a-movie",
115
+ f"{len(thin)} unit(s) hold one frame or none — a snapshot or an aborted "
116
+ "recording", thin))
117
+
118
+ no_conv, per_channel = [], defaultdict(dict)
119
+ for u in units:
120
+ for c in u.channels:
121
+ per_channel[c.name][u.path] = (c.offset, c.scale)
122
+ if c.offset == 0.0 and c.scale == 1.0:
123
+ no_conv.append(f"{u.path}/{c.name}")
124
+ if no_conv:
125
+ rep.findings.append(Finding(
126
+ "warn", "no-conversion",
127
+ f"{len(no_conv)} channel(s) carry no conversion attributes — their values "
128
+ "cannot be put in reader units and come back as stored", tuple(no_conv)))
129
+ for name, per_unit in sorted(per_channel.items()):
130
+ distinct = set(per_unit.values())
131
+ if len(distinct) > 1:
132
+ rep.findings.append(Finding(
133
+ "warn", "mixed-conversion",
134
+ f"{name} does not use one conversion across the file "
135
+ f"({len(distinct)} different) — convert per unit, never per file", ()))
136
+
137
+ counts = Counter(len(u.channels) for u in units)
138
+ if len(counts) > 1:
139
+ rep.findings.append(Finding(
140
+ "note", "mixed-channel-counts",
141
+ f"units carry different channel counts "
142
+ f"({', '.join(f'{n} ch x{k}' for n, k in sorted(counts.items()))})", ()))
143
+ return rep