From b37d9d9891f04d8db595d50df76c2b7eb92ee032 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:14:48 -0500 Subject: [PATCH 1/4] ci: add a PyPI release workflow, and modernize the license declaration This repo had no workflows at all, so nothing was built, checked, or published automatically -- and it is not on PyPI, while its README tells readers to pip install it. The workflow builds on every push and pull request via build-and-inspect-python-package (pinned to v3.0.1, whose Twine 7 understands the packaging metadata a PEP 639 license produces; the v2 line's Twine 6 does not), and publishes only on a published GitHub release. Publishing uses PyPI trusted publishing rather than a stored token: PyPI verifies the workflow's OIDC identity, so no long-lived secret exists to leak. It also attaches a build-provenance attestation. This requires a pending publisher to be configured on PyPI for the project name before the first release -- the workflow cannot create that, and the first release will fail without it. The license declaration is modernized in the same change, deliberately, because a release writes metadata to PyPI permanently and a version cannot be re-uploaded. The deprecated license table becomes a PEP 639 SPDX expression with license-files, and the redundant "License :: OSI Approved" classifier is removed -- current backends reject carrying both. Verified by building locally: the wheel now declares License-Expression: Apache-2.0 with the LICENSE captured, and passes twine check --strict. --- .github/workflows/cd.yml | 60 ++++ build/lib/dj_photon_codecs/__init__.py | 6 + build/lib/dj_photon_codecs/codecs.py | 278 ++++++++++++++++++ pyproject.toml | 6 +- src/dj_photon_codecs.egg-info/PKG-INFO | 165 +++++++++++ src/dj_photon_codecs.egg-info/SOURCES.txt | 11 + .../dependency_links.txt | 1 + .../entry_points.txt | 2 + src/dj_photon_codecs.egg-info/requires.txt | 11 + src/dj_photon_codecs.egg-info/top_level.txt | 1 + 10 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/cd.yml create mode 100644 build/lib/dj_photon_codecs/__init__.py create mode 100644 build/lib/dj_photon_codecs/codecs.py create mode 100644 src/dj_photon_codecs.egg-info/PKG-INFO create mode 100644 src/dj_photon_codecs.egg-info/SOURCES.txt create mode 100644 src/dj_photon_codecs.egg-info/dependency_links.txt create mode 100644 src/dj_photon_codecs.egg-info/entry_points.txt create mode 100644 src/dj_photon_codecs.egg-info/requires.txt create mode 100644 src/dj_photon_codecs.egg-info/top_level.txt diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..e9b9f61 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,60 @@ +name: CD + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 3 + +jobs: + dist: + name: Distribution build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # v3 ships Twine 7, which understands packaging metadata 2.4/2.5 — what a + # PEP 639 SPDX license declaration produces. v2 bundles Twine 6 and fails + # `twine check --strict`. v3 no longer force-tags majors, so pin in full. + - uses: hynek/build-and-inspect-python-package@v3.0.1 + + publish: + needs: [dist] + name: Publish to PyPI + environment: pypi + permissions: + # Trusted publishing: PyPI verifies this workflow's OIDC identity, so no + # API token is stored anywhere. Requires a pending publisher configured on + # PyPI for this project before the first release. + id-token: write + attestations: write + contents: read + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + - uses: actions/download-artifact@v8 + with: + name: Packages + path: dist + + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@v4 + with: + subject-path: "dist/*" + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/build/lib/dj_photon_codecs/__init__.py b/build/lib/dj_photon_codecs/__init__.py new file mode 100644 index 0000000..96dc6d1 --- /dev/null +++ b/build/lib/dj_photon_codecs/__init__.py @@ -0,0 +1,6 @@ +"""DataJoint codec for photon-limited movies with Anscombe transformation.""" + +from .codecs import PhotonCodec + +__version__ = "0.1.0" +__all__ = ["PhotonCodec"] diff --git a/build/lib/dj_photon_codecs/codecs.py b/build/lib/dj_photon_codecs/codecs.py new file mode 100644 index 0000000..5b8c127 --- /dev/null +++ b/build/lib/dj_photon_codecs/codecs.py @@ -0,0 +1,278 @@ +"""Codec for photon-limited movies with Anscombe transformation.""" + +from __future__ import annotations + +from typing import Any + +import numpy as np +import zarr + +try: + from anscombe import generalized_anscombe +except ImportError as e: + raise ImportError( + "anscombe-transform is required. Install with: pip install anscombe-transform" + ) from e + +try: + import datajoint as dj + from datajoint import DataJointError + from datajoint.builtin_codecs import SchemaCodec +except ImportError as e: + raise ImportError( + "datajoint>=2.0.0a22 is required. Install with: pip install 'datajoint>=2.0.0a22'" + ) from e + + +class PhotonCodec(SchemaCodec): + """ + Store photon-limited movies with Anscombe variance stabilization. + + The ```` codec applies Anscombe transformation to photon-limited + imaging data (Poisson noise) to stabilize variance, then stores in Zarr + format for efficient access. + + **Why Anscombe Transformation:** + - Converts Poisson-distributed photon counts to approximately Gaussian noise + - Stabilizes variance across intensity levels + - Enables better compression and denoising + - Standard preprocessing for low-light microscopy + + **Storage:** + - Transformed data stored in Zarr with movie-optimized chunking + - Metadata includes transform parameters for inverse operation + - Schema-addressed paths: ``{schema}/{table}/{pk}/{field}.zarr`` + + Example:: + + import datajoint as dj + import numpy as np + + schema = dj.Schema('calcium_imaging') + + @schema + class Recording(dj.Manual): + definition = ''' + recording_id : int + --- + movie : # Photon-limited movie with Anscombe transform + ''' + + # Insert photon-limited movie (raw photon counts) + movie = np.random.poisson(lam=10, size=(1000, 512, 512)) + Recording.insert1({ + 'recording_id': 1, + 'movie': movie, + }) + + # Fetch returns Zarr array (transformed data) + zarr_array = (Recording & {'recording_id': 1}).fetch1('movie') + + # Access movie frames efficiently + frame_100 = zarr_array[100] # Single frame + snippet = zarr_array[100:200] # Frame range + + # Apply inverse transform if needed + from anscombe import generalized_inverse_anscombe + original = generalized_inverse_anscombe(zarr_array[:]) + + Storage Structure:: + + {store_root}/{schema}/{table}/{pk}/{field}.zarr/ + .zarray # Zarr metadata + .zattrs # Transform parameters + 0.0.0, 0.1.0, ... # Chunks (time, y, x) + + Notes + ----- + - Input data should be raw photon counts (non-negative integers or floats) + - Transformed data is float64 with stabilized variance + - Chunking optimized for temporal access (process frames sequentially) + - For inverse transformation, use anscombe.generalized_inverse_anscombe() + + See Also + -------- + anscombe-transform : Anscombe variance stabilization library + dj-zarr-codecs : Zarr array storage codec + """ + + name = "photon" + CODEC_VERSION = "1.0" # Data format version for backward compatibility + + def validate(self, value: Any) -> None: + """ + Validate that value is a numpy array suitable for photon-limited data. + + Parameters + ---------- + value : Any + Value to validate. + + Raises + ------ + DataJointError + If value is not a numpy array, has object dtype, or contains + negative values (invalid for photon counts). + """ + if not isinstance(value, np.ndarray): + raise DataJointError( + f" requires numpy.ndarray, got {type(value).__name__}" + ) + if value.dtype == object: + raise DataJointError(" does not support object dtype arrays") + if value.ndim < 3: + raise DataJointError( + f" requires 3D+ arrays (time, height, width, ...), got {value.ndim}D" + ) + if np.any(value < 0): + raise DataJointError( + " requires non-negative values (photon counts cannot be negative)" + ) + + def encode( + self, + value: np.ndarray, + *, + key: dict | None = None, + store_name: str | None = None, + ) -> dict: + """ + Encode photon-limited movie with Anscombe transformation to Zarr. + + Parameters + ---------- + value : np.ndarray + Photon-limited movie data (3D+: time, height, width, ...). + Should be raw photon counts (non-negative). + key : dict, optional + Primary key values for path construction. + store_name : str, optional + Name of the object store to use. + + Returns + ------- + dict + Metadata stored in database: path, store, codec_version, shape, dtype, transform. + + Raises + ------ + DataJointError + If encoding fails. + """ + try: + # Validate input + self.validate(value) + + # Extract context from key + schema, table, field, primary_key = self._extract_context(key) + + # Build schema-addressed path + path, _token = self._build_path( + schema, table, field, primary_key, ext=".zarr", store_name=store_name + ) + + # Get storage backend + backend = self._get_backend(store_name) + + # Get fsspec mapper for Zarr write + store_map = backend.get_fsmap(path) + + # Apply Anscombe transformation + # Default parameters: gain=1, offset=0, variance=0 (Poisson noise) + transformed = generalized_anscombe(value, gain=1.0, offset=0.0, variance=0.0) + + # Optimize chunking for temporal access (movies) + # Chunk along time axis to enable efficient frame-by-frame processing + chunk_time = min(100, value.shape[0]) # Max 100 frames per chunk + chunks = (chunk_time,) + value.shape[1:] # Full spatial dimensions + + # Write array to Zarr with chunking and compression + zarr.save_array( + store_map, + transformed, + chunks=chunks, + compressor=zarr.Blosc(cname="zstd", clevel=5, shuffle=zarr.Blosc.BITSHUFFLE), + ) + + # Store transform parameters in Zarr attributes + z = zarr.open(store_map, mode="r+") + z.attrs["codec_version"] = self.CODEC_VERSION + z.attrs["codec_name"] = self.name + z.attrs["anscombe_gain"] = 1.0 + z.attrs["anscombe_offset"] = 0.0 + z.attrs["anscombe_variance"] = 0.0 + z.attrs["original_dtype"] = str(value.dtype) + + # Return metadata for database storage + return { + "path": path, + "store": store_name, + "codec_version": self.CODEC_VERSION, + "shape": list(value.shape), + "dtype": str(transformed.dtype), + "transform": "anscombe", + } + + except Exception as e: + raise DataJointError(f"Failed to encode photon movie: {e}") from e + + def decode(self, stored: dict, *, key: dict | None = None) -> zarr.Array: + """ + Decode photon-limited movie from Zarr storage. + + Parameters + ---------- + stored : dict + Metadata from database containing path and store. + key : dict, optional + Primary key values (unused). + + Returns + ------- + zarr.Array + Read-only Zarr array containing Anscombe-transformed data. + Use ``anscombe.generalized_inverse_anscombe()`` to recover + original photon counts if needed. + + Notes + ----- + The returned array contains transformed data. To get original scale: + + >>> from anscombe import generalized_inverse_anscombe + >>> zarr_array = (MyTable & key).fetch1('movie') + >>> original = generalized_inverse_anscombe(zarr_array[:]) + + Transform parameters are stored in ``zarr_array.attrs``. + + Raises + ------ + DataJointError + If decoding fails. + """ + try: + # Get storage backend + backend = self._get_backend(stored.get("store")) + + # Get fsspec mapper for Zarr path + store_map = backend.get_fsmap(stored["path"]) + + # Open Zarr array (read-only) + z = zarr.open(store_map, mode="r") + + # Check codec version for backward compatibility + # Priority: Zarr attrs > DB metadata > default "1.0" + version = z.attrs.get( + "codec_version", stored.get("codec_version", "1.0") + ) + + # All v1.x versions are compatible + if version.startswith("1."): + return z + else: + raise DataJointError( + f"Unsupported photon codec version: {version}. " + f"Upgrade dj-photon-codecs or migrate data." + ) + + except Exception as e: + raise DataJointError(f"Failed to decode photon movie: {e}") from e diff --git a/pyproject.toml b/pyproject.toml index b6b725b..3f7bbef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ version = "0.1.0" description = "DataJoint codec for photon-limited movies with Anscombe transformation and compression" readme = "README.md" requires-python = ">=3.10" -license = {text = "Apache-2.0"} +license = "Apache-2.0" +license-files = ["LICENSE"] authors = [ {name = "Dimitri Yatsenko", email = "dimitri@datajoint.com"}, {name = "Davis Bennett", email = "davis.v.bennett@gmail.com"}, @@ -13,7 +14,6 @@ keywords = ["datajoint", "photon", "anscombe", "imaging", "codec"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -47,7 +47,7 @@ Documentation = "https://dj-photon-codecs.readthedocs.io/" photon = "dj_photon_codecs:PhotonCodec" [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] diff --git a/src/dj_photon_codecs.egg-info/PKG-INFO b/src/dj_photon_codecs.egg-info/PKG-INFO new file mode 100644 index 0000000..1fc9f7d --- /dev/null +++ b/src/dj_photon_codecs.egg-info/PKG-INFO @@ -0,0 +1,165 @@ +Metadata-Version: 2.4 +Name: dj-photon-codecs +Version: 0.1.0 +Summary: DataJoint codec for photon-limited movies with Anscombe transformation and compression +Author-email: Dimitri Yatsenko , Davis Bennett +License-Expression: Apache-2.0 +Project-URL: Homepage, https://github.com/datajoint/dj-photon-codecs +Project-URL: Repository, https://github.com/datajoint/dj-photon-codecs +Project-URL: Documentation, https://dj-photon-codecs.readthedocs.io/ +Project-URL: Bug Tracker, https://github.com/datajoint/dj-photon-codecs/issues +Keywords: datajoint,photon,anscombe,imaging,codec +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Science/Research +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Scientific/Engineering +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: datajoint>=2.0.0a22 +Requires-Dist: zarr>=2.0 +Requires-Dist: numpy>=1.20 +Requires-Dist: anscombe-transform>=1.0 +Provides-Extra: dev +Requires-Dist: pytest>=7.0; extra == "dev" +Requires-Dist: pytest-cov>=4.0; extra == "dev" +Requires-Dist: ruff>=0.1.0; extra == "dev" +Requires-Dist: mkdocs-material>=9.0; extra == "dev" +Requires-Dist: mkdocstrings[python]>=0.24; extra == "dev" +Dynamic: license-file + +# dj-photon-codecs + +DataJoint codec for photon-limited movies with Anscombe variance stabilization and compressed Zarr storage. + +[![PyPI version](https://badge.fury.io/py/dj-photon-codecs.svg)](https://pypi.org/project/dj-photon-codecs/) +[![Documentation](https://readthedocs.org/projects/dj-photon-codecs/badge/?version=latest)](https://dj-photon-codecs.readthedocs.io/) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +## Overview + +This codec enables efficient storage of photon-limited imaging data (calcium imaging, fluorescence microscopy) by: + +1. **Variance stabilization** - Anscombe transformation converts Poisson noise to constant variance +2. **High compression** - Blosc/Zstd achieves 3-5x compression on stabilized data +3. **Efficient access** - Zarr format with temporal chunking for frame-by-frame processing + +## Quick Start + +### Installation + +```bash +pip install dj-photon-codecs +``` + +### Basic Usage + +```python +import datajoint as dj +import numpy as np + +# Configure object storage +dj.config['stores'] = { + 'imaging': { + 'protocol': 's3', + 'bucket': 'my-data', + } +} + +# Define table +schema = dj.Schema('calcium_imaging') + +@schema +class Recording(dj.Manual): + definition = """ + recording_id : int32 + --- + movie : # Photon-limited movie + """ + +# Insert raw photon counts +movie = np.random.poisson(lam=10, size=(1000, 512, 512)) +Recording.insert1({'recording_id': 1, 'movie': movie}) + +# Fetch returns Zarr array (Anscombe-transformed) +zarr_array = (Recording & {'recording_id': 1}).fetch1('movie') +frame = zarr_array[100] # Efficient frame access +``` + +### Apply Inverse Transform + +```python +from anscombe import generalized_inverse_anscombe + +# Recover original photon counts +original = generalized_inverse_anscombe(zarr_array[:]) +``` + +## Features + +- **Automatic variance stabilization** using Anscombe transformation +- **3-5x compression** with Blosc/Zstd on variance-stabilized data +- **Temporal chunking** optimized for sequential frame access +- **Schema-addressed paths** mirroring database structure +- **Lazy loading** - access frames without loading entire movie +- **Invertible transformation** - mathematically lossless + +## When to Use + +✅ **Use for:** +- Photon-limited imaging (calcium imaging, fluorescence microscopy) +- Data where Poisson shot noise dominates +- Large movies requiring efficient storage +- Sequential frame processing workflows + +❌ **Don't use for:** +- Preprocessed/normalized data (ΔF/F, z-scored) +- Data with negative values +- Non-Poisson noise dominates + +## Documentation + +📚 **[Full Documentation](https://dj-photon-codecs.readthedocs.io/)** + +- [Installation Guide](https://dj-photon-codecs.readthedocs.io/en/latest/getting-started/installation/) +- [Quick Start Tutorial](https://dj-photon-codecs.readthedocs.io/en/latest/getting-started/quick-start/) +- [User Guide](https://dj-photon-codecs.readthedocs.io/en/latest/user-guide/overview/) +- [API Reference](https://dj-photon-codecs.readthedocs.io/en/latest/api/reference/) +- [Examples](https://dj-photon-codecs.readthedocs.io/en/latest/examples/calcium-imaging/) + +## Contributing + +Contributions are welcome! See our [Contributing Guide](https://dj-photon-codecs.readthedocs.io/en/latest/contributing/). + +```bash +# Development setup +git clone https://github.com/datajoint/dj-photon-codecs.git +cd dj-photon-codecs +pip install -e ".[dev]" + +# Run tests +pytest + +# Build docs +mkdocs serve +``` + +## Support + +- 📖 [Documentation](https://dj-photon-codecs.readthedocs.io/) +- 💬 [GitHub Discussions](https://github.com/datajoint/dj-photon-codecs/discussions) - Questions and community +- 🐛 [GitHub Issues](https://github.com/datajoint/dj-photon-codecs/issues) - Bug reports and feature requests + +## Related Projects + +- [DataJoint](https://datajoint.com) - Scientific data pipeline framework +- [anscombe-transform](https://github.com/datajoint/anscombe-transform) - Variance stabilization library +- [dj-zarr-codecs](https://github.com/datajoint/dj-zarr-codecs) - General Zarr array codec +- [Zarr](https://zarr.dev/) - Chunked, compressed array storage + +## License + +Apache License 2.0. Copyright (c) 2026 DataJoint Inc. diff --git a/src/dj_photon_codecs.egg-info/SOURCES.txt b/src/dj_photon_codecs.egg-info/SOURCES.txt new file mode 100644 index 0000000..76599ef --- /dev/null +++ b/src/dj_photon_codecs.egg-info/SOURCES.txt @@ -0,0 +1,11 @@ +LICENSE +README.md +pyproject.toml +src/dj_photon_codecs/__init__.py +src/dj_photon_codecs/codecs.py +src/dj_photon_codecs.egg-info/PKG-INFO +src/dj_photon_codecs.egg-info/SOURCES.txt +src/dj_photon_codecs.egg-info/dependency_links.txt +src/dj_photon_codecs.egg-info/entry_points.txt +src/dj_photon_codecs.egg-info/requires.txt +src/dj_photon_codecs.egg-info/top_level.txt \ No newline at end of file diff --git a/src/dj_photon_codecs.egg-info/dependency_links.txt b/src/dj_photon_codecs.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/dj_photon_codecs.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/dj_photon_codecs.egg-info/entry_points.txt b/src/dj_photon_codecs.egg-info/entry_points.txt new file mode 100644 index 0000000..181693c --- /dev/null +++ b/src/dj_photon_codecs.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[datajoint.codecs] +photon = dj_photon_codecs:PhotonCodec diff --git a/src/dj_photon_codecs.egg-info/requires.txt b/src/dj_photon_codecs.egg-info/requires.txt new file mode 100644 index 0000000..15badbe --- /dev/null +++ b/src/dj_photon_codecs.egg-info/requires.txt @@ -0,0 +1,11 @@ +datajoint>=2.0.0a22 +zarr>=2.0 +numpy>=1.20 +anscombe-transform>=1.0 + +[dev] +pytest>=7.0 +pytest-cov>=4.0 +ruff>=0.1.0 +mkdocs-material>=9.0 +mkdocstrings[python]>=0.24 diff --git a/src/dj_photon_codecs.egg-info/top_level.txt b/src/dj_photon_codecs.egg-info/top_level.txt new file mode 100644 index 0000000..37110b8 --- /dev/null +++ b/src/dj_photon_codecs.egg-info/top_level.txt @@ -0,0 +1 @@ +dj_photon_codecs From e3b45f4696b62c5afb7d388808f706caef4ad6ae Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:26:40 -0500 Subject: [PATCH 2/4] Declare the Python range datajoint actually supports Two problems, both making the package claim something untrue. The classifiers stopped at 3.12. datajoint 2.3.2 classifies 3.10 through 3.14, so this package was telling PyPI it did not support the two most recent Pythons, including the current stable release. There was no upper bound. datajoint pins <3.15; this package did not, so pip would happily install it on 3.15 into an environment where its only real dependency cannot be installed at all. requires-python is now >=3.10,<3.15, matching datajoint exactly rather than approximating it -- the floor and the ceiling both come from the dependency that determines them. Verified by building: Requires-Python: <3.15,>=3.10, classifiers through 3.14, and twine check --strict passes. --- pyproject.toml | 4 +++- src/dj_photon_codecs.egg-info/PKG-INFO | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f7bbef..d8cd517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "dj-photon-codecs" version = "0.1.0" description = "DataJoint codec for photon-limited movies with Anscombe transformation and compression" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.15" license = "Apache-2.0" license-files = ["LICENSE"] authors = [ @@ -18,6 +18,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] diff --git a/src/dj_photon_codecs.egg-info/PKG-INFO b/src/dj_photon_codecs.egg-info/PKG-INFO index 1fc9f7d..048c971 100644 --- a/src/dj_photon_codecs.egg-info/PKG-INFO +++ b/src/dj_photon_codecs.egg-info/PKG-INFO @@ -15,8 +15,10 @@ Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Scientific/Engineering -Requires-Python: >=3.10 +Requires-Python: <3.15,>=3.10 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: datajoint>=2.0.0a22 From 8d381fef0c91eef2f0edad62322ff0658af4fd71 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:30:34 -0500 Subject: [PATCH 3/4] Drop the upper bound on requires-python; keep the classifiers The cap I added is unnecessary and would cause the failure mode it was meant to prevent. Removing it. My justification for it was wrong. I claimed that without a cap, pip on 3.15 would install this package into an environment where datajoint cannot be installed. It will not: this package pins datajoint>=2.0, and a dependency's requires-python is honored during resolution, so pip fails there with an error that names datajoint -- the accurate diagnosis. Verified by resolving into a 3.9 environment: `datajoint>=2.0` fails cleanly with "datajoint>=2.0.0 cannot be used", while bare `datajoint` silently resolves to 0.14.9, an ancient release predating 2.0. That silent downgrade is the argument against caps. requires-python is a resolution input, so a capped release is skipped rather than reported: on a Python the cap excludes, pip installs an older release of this package instead of saying why. And a published cap cannot be relaxed -- when 3.15 ships and works, every capped version still refuses it, and support requires a new release rather than nothing at all. The classifiers stay at 3.10 through 3.14, which is the range actually tested and claimed. Classifiers are documentation and carry no resolution behavior, so they can say what is verified without constraining what is possible. --- pyproject.toml | 2 +- src/dj_photon_codecs.egg-info/PKG-INFO | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d8cd517..6f24f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "dj-photon-codecs" version = "0.1.0" description = "DataJoint codec for photon-limited movies with Anscombe transformation and compression" readme = "README.md" -requires-python = ">=3.10,<3.15" +requires-python = ">=3.10" license = "Apache-2.0" license-files = ["LICENSE"] authors = [ diff --git a/src/dj_photon_codecs.egg-info/PKG-INFO b/src/dj_photon_codecs.egg-info/PKG-INFO index 048c971..9034926 100644 --- a/src/dj_photon_codecs.egg-info/PKG-INFO +++ b/src/dj_photon_codecs.egg-info/PKG-INFO @@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Scientific/Engineering -Requires-Python: <3.15,>=3.10 +Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: datajoint>=2.0.0a22 From b2a557c7634b80e96f1546b1df84b28e5f9d2fef Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:33:57 -0500 Subject: [PATCH 4/4] Derive the version from the git tag, mirroring dj-zarr-codecs The version was declared statically in pyproject.toml, and in this package's __init__.py as well, so two files had to agree with each other and with whatever tag a release carried. Nothing enforced that, and a release where they disagree is the kind of bug that is only visible after publishing. Now hatchling plus hatch-vcs, matching dj-zarr-codecs: the git tag is the single source of truth, the build hook writes _version.py, and the package imports it. A release is a tag, and no pull request touches a version number -- which is what we want anyway. Consequence worth stating plainly: the previously declared version is gone. Until a tag exists the build produces a development version derived from the commit distance, and the first tag is what fixes the number. That tag is a decision, not a carry-over. Also adds a .gitignore, which this repo did not have, covering the generated _version.py and the usual build artifacts. --- .gitignore | 13 ++ pyproject.toml | 15 +- src/dj_photon_codecs.egg-info/PKG-INFO | 167 ------------------ src/dj_photon_codecs.egg-info/SOURCES.txt | 11 -- .../dependency_links.txt | 1 - .../entry_points.txt | 2 - src/dj_photon_codecs.egg-info/requires.txt | 11 -- src/dj_photon_codecs.egg-info/top_level.txt | 1 - src/dj_photon_codecs/__init__.py | 4 +- 9 files changed, 27 insertions(+), 198 deletions(-) create mode 100644 .gitignore delete mode 100644 src/dj_photon_codecs.egg-info/PKG-INFO delete mode 100644 src/dj_photon_codecs.egg-info/SOURCES.txt delete mode 100644 src/dj_photon_codecs.egg-info/dependency_links.txt delete mode 100644 src/dj_photon_codecs.egg-info/entry_points.txt delete mode 100644 src/dj_photon_codecs.egg-info/requires.txt delete mode 100644 src/dj_photon_codecs.egg-info/top_level.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6617e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Generated by the hatch-vcs build hook — the git tag is the source of truth +src/**/_version.py + +# Build artifacts +build/ +dist/ +*.egg-info/ +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ diff --git a/pyproject.toml b/pyproject.toml index 6f24f77..420a4df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "dj-photon-codecs" -version = "0.1.0" description = "DataJoint codec for photon-limited movies with Anscombe transformation and compression" readme = "README.md" requires-python = ">=3.10" +dynamic = ["version"] license = "Apache-2.0" license-files = ["LICENSE"] authors = [ @@ -49,8 +49,8 @@ Documentation = "https://dj-photon-codecs.readthedocs.io/" photon = "dj_photon_codecs:PhotonCodec" [build-system] -requires = ["setuptools>=77"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.27", "hatch-vcs"] +build-backend = "hatchling.build" [tool.setuptools.packages.find] where = ["src"] @@ -66,3 +66,12 @@ target-version = "py310" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP"] ignore = ["E501"] # line too long (handled by formatter) + +# The git tag is the single source of truth for the version — mirrors dj-zarr-codecs. +# The build hook writes _version.py, which the package imports, so nothing declares a +# version twice and nothing needs bumping in a pull request. +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "src/dj_photon_codecs/_version.py" diff --git a/src/dj_photon_codecs.egg-info/PKG-INFO b/src/dj_photon_codecs.egg-info/PKG-INFO deleted file mode 100644 index 9034926..0000000 --- a/src/dj_photon_codecs.egg-info/PKG-INFO +++ /dev/null @@ -1,167 +0,0 @@ -Metadata-Version: 2.4 -Name: dj-photon-codecs -Version: 0.1.0 -Summary: DataJoint codec for photon-limited movies with Anscombe transformation and compression -Author-email: Dimitri Yatsenko , Davis Bennett -License-Expression: Apache-2.0 -Project-URL: Homepage, https://github.com/datajoint/dj-photon-codecs -Project-URL: Repository, https://github.com/datajoint/dj-photon-codecs -Project-URL: Documentation, https://dj-photon-codecs.readthedocs.io/ -Project-URL: Bug Tracker, https://github.com/datajoint/dj-photon-codecs/issues -Keywords: datajoint,photon,anscombe,imaging,codec -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: Science/Research -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Classifier: Topic :: Scientific/Engineering -Requires-Python: >=3.10 -Description-Content-Type: text/markdown -License-File: LICENSE -Requires-Dist: datajoint>=2.0.0a22 -Requires-Dist: zarr>=2.0 -Requires-Dist: numpy>=1.20 -Requires-Dist: anscombe-transform>=1.0 -Provides-Extra: dev -Requires-Dist: pytest>=7.0; extra == "dev" -Requires-Dist: pytest-cov>=4.0; extra == "dev" -Requires-Dist: ruff>=0.1.0; extra == "dev" -Requires-Dist: mkdocs-material>=9.0; extra == "dev" -Requires-Dist: mkdocstrings[python]>=0.24; extra == "dev" -Dynamic: license-file - -# dj-photon-codecs - -DataJoint codec for photon-limited movies with Anscombe variance stabilization and compressed Zarr storage. - -[![PyPI version](https://badge.fury.io/py/dj-photon-codecs.svg)](https://pypi.org/project/dj-photon-codecs/) -[![Documentation](https://readthedocs.org/projects/dj-photon-codecs/badge/?version=latest)](https://dj-photon-codecs.readthedocs.io/) -[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) - -## Overview - -This codec enables efficient storage of photon-limited imaging data (calcium imaging, fluorescence microscopy) by: - -1. **Variance stabilization** - Anscombe transformation converts Poisson noise to constant variance -2. **High compression** - Blosc/Zstd achieves 3-5x compression on stabilized data -3. **Efficient access** - Zarr format with temporal chunking for frame-by-frame processing - -## Quick Start - -### Installation - -```bash -pip install dj-photon-codecs -``` - -### Basic Usage - -```python -import datajoint as dj -import numpy as np - -# Configure object storage -dj.config['stores'] = { - 'imaging': { - 'protocol': 's3', - 'bucket': 'my-data', - } -} - -# Define table -schema = dj.Schema('calcium_imaging') - -@schema -class Recording(dj.Manual): - definition = """ - recording_id : int32 - --- - movie : # Photon-limited movie - """ - -# Insert raw photon counts -movie = np.random.poisson(lam=10, size=(1000, 512, 512)) -Recording.insert1({'recording_id': 1, 'movie': movie}) - -# Fetch returns Zarr array (Anscombe-transformed) -zarr_array = (Recording & {'recording_id': 1}).fetch1('movie') -frame = zarr_array[100] # Efficient frame access -``` - -### Apply Inverse Transform - -```python -from anscombe import generalized_inverse_anscombe - -# Recover original photon counts -original = generalized_inverse_anscombe(zarr_array[:]) -``` - -## Features - -- **Automatic variance stabilization** using Anscombe transformation -- **3-5x compression** with Blosc/Zstd on variance-stabilized data -- **Temporal chunking** optimized for sequential frame access -- **Schema-addressed paths** mirroring database structure -- **Lazy loading** - access frames without loading entire movie -- **Invertible transformation** - mathematically lossless - -## When to Use - -✅ **Use for:** -- Photon-limited imaging (calcium imaging, fluorescence microscopy) -- Data where Poisson shot noise dominates -- Large movies requiring efficient storage -- Sequential frame processing workflows - -❌ **Don't use for:** -- Preprocessed/normalized data (ΔF/F, z-scored) -- Data with negative values -- Non-Poisson noise dominates - -## Documentation - -📚 **[Full Documentation](https://dj-photon-codecs.readthedocs.io/)** - -- [Installation Guide](https://dj-photon-codecs.readthedocs.io/en/latest/getting-started/installation/) -- [Quick Start Tutorial](https://dj-photon-codecs.readthedocs.io/en/latest/getting-started/quick-start/) -- [User Guide](https://dj-photon-codecs.readthedocs.io/en/latest/user-guide/overview/) -- [API Reference](https://dj-photon-codecs.readthedocs.io/en/latest/api/reference/) -- [Examples](https://dj-photon-codecs.readthedocs.io/en/latest/examples/calcium-imaging/) - -## Contributing - -Contributions are welcome! See our [Contributing Guide](https://dj-photon-codecs.readthedocs.io/en/latest/contributing/). - -```bash -# Development setup -git clone https://github.com/datajoint/dj-photon-codecs.git -cd dj-photon-codecs -pip install -e ".[dev]" - -# Run tests -pytest - -# Build docs -mkdocs serve -``` - -## Support - -- 📖 [Documentation](https://dj-photon-codecs.readthedocs.io/) -- 💬 [GitHub Discussions](https://github.com/datajoint/dj-photon-codecs/discussions) - Questions and community -- 🐛 [GitHub Issues](https://github.com/datajoint/dj-photon-codecs/issues) - Bug reports and feature requests - -## Related Projects - -- [DataJoint](https://datajoint.com) - Scientific data pipeline framework -- [anscombe-transform](https://github.com/datajoint/anscombe-transform) - Variance stabilization library -- [dj-zarr-codecs](https://github.com/datajoint/dj-zarr-codecs) - General Zarr array codec -- [Zarr](https://zarr.dev/) - Chunked, compressed array storage - -## License - -Apache License 2.0. Copyright (c) 2026 DataJoint Inc. diff --git a/src/dj_photon_codecs.egg-info/SOURCES.txt b/src/dj_photon_codecs.egg-info/SOURCES.txt deleted file mode 100644 index 76599ef..0000000 --- a/src/dj_photon_codecs.egg-info/SOURCES.txt +++ /dev/null @@ -1,11 +0,0 @@ -LICENSE -README.md -pyproject.toml -src/dj_photon_codecs/__init__.py -src/dj_photon_codecs/codecs.py -src/dj_photon_codecs.egg-info/PKG-INFO -src/dj_photon_codecs.egg-info/SOURCES.txt -src/dj_photon_codecs.egg-info/dependency_links.txt -src/dj_photon_codecs.egg-info/entry_points.txt -src/dj_photon_codecs.egg-info/requires.txt -src/dj_photon_codecs.egg-info/top_level.txt \ No newline at end of file diff --git a/src/dj_photon_codecs.egg-info/dependency_links.txt b/src/dj_photon_codecs.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/dj_photon_codecs.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/dj_photon_codecs.egg-info/entry_points.txt b/src/dj_photon_codecs.egg-info/entry_points.txt deleted file mode 100644 index 181693c..0000000 --- a/src/dj_photon_codecs.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[datajoint.codecs] -photon = dj_photon_codecs:PhotonCodec diff --git a/src/dj_photon_codecs.egg-info/requires.txt b/src/dj_photon_codecs.egg-info/requires.txt deleted file mode 100644 index 15badbe..0000000 --- a/src/dj_photon_codecs.egg-info/requires.txt +++ /dev/null @@ -1,11 +0,0 @@ -datajoint>=2.0.0a22 -zarr>=2.0 -numpy>=1.20 -anscombe-transform>=1.0 - -[dev] -pytest>=7.0 -pytest-cov>=4.0 -ruff>=0.1.0 -mkdocs-material>=9.0 -mkdocstrings[python]>=0.24 diff --git a/src/dj_photon_codecs.egg-info/top_level.txt b/src/dj_photon_codecs.egg-info/top_level.txt deleted file mode 100644 index 37110b8..0000000 --- a/src/dj_photon_codecs.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -dj_photon_codecs diff --git a/src/dj_photon_codecs/__init__.py b/src/dj_photon_codecs/__init__.py index 96dc6d1..3d60709 100644 --- a/src/dj_photon_codecs/__init__.py +++ b/src/dj_photon_codecs/__init__.py @@ -2,5 +2,5 @@ from .codecs import PhotonCodec -__version__ = "0.1.0" -__all__ = ["PhotonCodec"] +from ._version import version as __version__ +__all__ = ["PhotonCodec", "__version__"]