From d278b9f93105733cb920a9c5f48b0f797dfa736d Mon Sep 17 00:00:00 2001 From: Jordao Bragantini Date: Mon, 15 Jun 2026 10:43:21 -0700 Subject: [PATCH 1/4] Add CI and release workflows Adapt the tracksdata workflows for hoct (no docs/benchmarks, no Qt): - ci.yml: lint (ruff pinned to the pre-commit baseline) + test matrix on Python 3.11/3.12 with coverage. GEFF fixtures, model weights, and a Gurobi license are not provisioned in CI, so the data/model/solver tests skip while the unit tests and the local-server download mechanism run. - release.yml: on `v*` tags, build with uv and publish to PyPI via trusted publishing (OIDC) plus a GitHub release; `workflow_dispatch` supports a dry-run. fetch-depth: 0 so hatch-vcs derives the version from the tag. Also gitignore coverage.xml. --- .github/workflows/ci.yml | 62 ++++++++++++++++++++ .github/workflows/release.yml | 107 ++++++++++++++++++++++++++++++++++ .gitignore | 1 + 3 files changed, 170 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d93cc26 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + # Pin ruff to the version used by the pre-commit hooks so CI matches the + # project's lint baseline. + - name: Lint with ruff + run: | + uvx ruff@0.11.12 check . + uvx ruff@0.11.12 format --check . + + test: + needs: lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --extra dev + + # GEFF fixtures, model weights, and a Gurobi license are not provisioned in + # CI, so the data/model/solver tests skip; the unit tests and the + # download mechanism (served from a local HTTP server) still run. + - name: Run tests with coverage + run: uv run pytest --cov=hoct --cov-report=xml -q + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.12' && github.repository == 'royerlab/hoct' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b20c260 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,107 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + test_release: + description: 'Test release (skips PyPI publish)' + required: false + default: false + type: boolean + +permissions: + contents: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # required for hatch-vcs to read git tags/history + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: "3.12" + + - name: Install dependencies + run: uv sync --extra dev + + - name: Run tests + run: uv run pytest -q + + - name: Build package + run: uv build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + release: + needs: build + runs-on: ubuntu-latest + # Only publish from the main repo and not for dry-run releases. + if: github.repository == 'royerlab/hoct' && github.event.inputs.test_release != 'true' + environment: + name: pypi + url: https://pypi.org/p/hoct + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: true + + test-release: + needs: build + runs-on: ubuntu-latest + # Dry run for workflow_dispatch test releases or forks. + if: github.event.inputs.test_release == 'true' || github.repository != 'royerlab/hoct' + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Test PyPI upload (dry run) + run: | + echo "🧪 This would upload to PyPI:" + ls -la dist/ + echo "✅ Test release completed successfully!" + + github-release: + needs: build + runs-on: ubuntu-latest + if: github.repository == 'royerlab/hoct' && github.event.inputs.test_release != 'true' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true + draft: false + prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }} diff --git a/.gitignore b/.gitignore index 446e420..c510ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ env/ # Testing .pytest_cache/ .coverage +coverage.xml htmlcov/ .tox/ From 05922765d70fabd04ed7739b67ee0e0e58b0de36 Mon Sep 17 00:00:00 2001 From: Jordao Bragantini Date: Mon, 15 Jun 2026 10:52:31 -0700 Subject: [PATCH 2/4] CI: bump actions to Node 24 majors; key uv cache on pyproject --- .github/workflows/ci.yml | 10 ++++++---- .github/workflows/release.yml | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d93cc26..f98437f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,13 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "pyproject.toml" # Pin ruff to the version used by the pre-commit hooks so CI matches the # project's lint baseline. @@ -37,12 +38,13 @@ jobs: python-version: ["3.11", "3.12"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "pyproject.toml" python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b20c260..256f9bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,14 +21,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 # required for hatch-vcs to read git tags/history - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "pyproject.toml" python-version: "3.12" - name: Install dependencies @@ -90,7 +91,7 @@ jobs: if: github.repository == 'royerlab/hoct' && github.event.inputs.test_release != 'true' steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download build artifacts uses: actions/download-artifact@v4 From e3c44fc6656aa02fcdf629e4b838b28519746670 Mon Sep 17 00:00:00 2001 From: Jordao Bragantini Date: Mon, 15 Jun 2026 11:02:10 -0700 Subject: [PATCH 3/4] removing summary print --- src/hoct/tracking/_tracklet_solver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hoct/tracking/_tracklet_solver.py b/src/hoct/tracking/_tracklet_solver.py index 01b088e..b9fa6b3 100644 --- a/src/hoct/tracking/_tracklet_solver.py +++ b/src/hoct/tracking/_tracklet_solver.py @@ -157,8 +157,6 @@ def _tracklet_graph( list(tracklet_edges_df.iter_rows(named=True)), ) - tracklet_graph.summary(attrs_stats=True) - tracklet_invalid_edges = tracklet_graph.edge_attrs().filter(pl.col("delta_t") <= 0) if len(tracklet_invalid_edges) > 0: raise ValueError( From d8d6434a110509691f0ba24a4495c44c358e94f1 Mon Sep 17 00:00:00 2001 From: Jordao Bragantini Date: Mon, 15 Jun 2026 12:20:45 -0700 Subject: [PATCH 4/4] adding normalization argument --- src/hoct/features/features.py | 17 +++++++++++++++-- src/hoct/features/graph.py | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/hoct/features/features.py b/src/hoct/features/features.py index 9492b5c..65bcb63 100644 --- a/src/hoct/features/features.py +++ b/src/hoct/features/features.py @@ -57,8 +57,21 @@ def normalize_image( clip: bool = False, uq: float = 0.999, ) -> NDArray: - """ - Normalize an image to [0, 1] using the quantile. + """Normalize an image to ``[0, 1]`` using an upper quantile. + + Parameters + ---------- + image : NDArray + Image to normalize. + clip : bool, default=False + If True, clip the normalized values to ``[0, 1]``. + uq : float, default=0.999 + Upper quantile used as the normalization maximum (robust to outliers). + + Returns + ------- + NDArray + The normalized image as ``float32``. """ image = np.asarray(image, dtype=np.float32) diff --git a/src/hoct/features/graph.py b/src/hoct/features/graph.py index ee189fb..64de854 100644 --- a/src/hoct/features/graph.py +++ b/src/hoct/features/graph.py @@ -1,4 +1,5 @@ from functools import partial +from typing import Any import dask.array as da import numpy as np @@ -9,7 +10,7 @@ from tracksdata.utils._multiprocessing import multiprocessing_apply from hoct.features.constants import EDGE_GT_KEY, REGIONPROPS -from hoct.features.features import add_border_dist, add_delta_t, add_is_div +from hoct.features.features import add_border_dist, add_delta_t, add_is_div, normalize_image def convert_to_3d(graph: td.graph.RustWorkXGraph) -> None: @@ -69,6 +70,8 @@ def create_graph( delta_t: float, scale: tuple[float, ...] | None = None, images: ArrayLike | None = None, + normalize_images: bool = True, + normalize_kwargs: dict[str, Any] | None = None, gt_graph: td.graph.BaseGraph | None = None, out_graph: td.graph.BaseGraph | None = None, ) -> td.graph.InMemoryGraph: @@ -92,6 +95,13 @@ def create_graph( images : ArrayLike | None Optional intensity images of shape (T, [Z,] Y, X). If None, only geometric features are computed. + normalize_images : bool, default=True + If True and ``images`` is provided, normalize each time point with + :func:`hoct.features.normalize_image` before extracting intensity + features. Has no effect when ``images`` is None. + normalize_kwargs : dict[str, Any] | None + Keyword arguments forwarded to :func:`hoct.features.normalize_image` + (e.g. ``clip``, ``uq``). Defaults to an empty dict. gt_graph : td.graph.BaseGraph | None Optional ground truth graph for training. If provided, adds ground truth edge labels. If None (inference mode), skips ground truth-related features. @@ -123,6 +133,12 @@ def create_graph( if images is not None and not isinstance(images, da.Array): images = da.from_array(images) + if images is not None and normalize_images: + images = images.rechunk((1, *images.shape[1:])) + if normalize_kwargs is None: + normalize_kwargs = {} + images = images.map_blocks(normalize_image, **normalize_kwargs) + if scale is None: scale = (1.0,) * labels.ndim