Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/scripts/install_torch_cpu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Installs CPU-only torch from the PyTorch index with retries.
#
# That index (download.pytorch.org/whl/cpu, Cloudflare R2-backed as
# download-r2.pytorch.org) intermittently drops the TLS handshake
# (SSLV3_ALERT_HANDSHAKE_FAILURE) from GitHub-hosted runners. pip's own
# built-in retries reuse the same connection pool within one process and
# do not help here; a fresh `pip install` invocation gets a fresh TLS
# connection and has in practice succeeded where the retries within a
# single invocation did not. Five attempts with linear backoff (10s, 20s,
# 30s, 40s) before giving up.
set -euo pipefail

max_attempts=5
attempt=1
until pip install torch --index-url https://download.pytorch.org/whl/cpu; do
if [ "${attempt}" -ge "${max_attempts}" ]; then
echo "::error::torch install failed after ${max_attempts} attempts" >&2
exit 1
fi
sleep_seconds=$((attempt * 10))
echo "torch install attempt ${attempt}/${max_attempts} failed; retrying in ${sleep_seconds}s" >&2
sleep "${sleep_seconds}"
attempt=$((attempt + 1))
done
72 changes: 72 additions & 0 deletions .github/workflows/auto-tag-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Auto Tag Release

# Creates the tag for a release-cut merge to main; it does nothing else.
# release.yml (build, attestations, PyPI publish, signing, GitHub Release)
# is triggered solely by that tag push, exactly as it would be from a
# manual `git tag vX.Y.Z && git push origin vX.Y.Z`. Keeping tag creation
# and publishing in two separate, single-purpose workflows means the
# actual publish path has exactly one trigger (a tag push) to reason
# about, instead of depending on GitHub's anti-recursion behavior for
# GITHUB_TOKEN-authored pushes not re-triggering the same push event.
#
# If a version needs a tag this workflow's format check rejects (see
# below), tag it manually -- this automation only ever creates a tag
# when one is missing, so a pre-existing manual tag always takes
# precedence and this workflow becomes a no-op for that version.

on:
push:
branches: [main]
paths:
- pyproject.toml

permissions:
contents: read

jobs:
tag:
runs-on: ubuntu-latest
permissions:
contents: write # push the tag; nothing else in this workflow needs it
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # need full tag history to check for an existing tag

- name: Create the release tag if this version doesn't have one yet
run: |
set -euo pipefail

version=$(grep -m1 '^version = "' pyproject.toml | sed -E 's/version = "(.*)"/\1/')
if [ -z "${version}" ]; then
echo "::error::could not extract a version from pyproject.toml"
exit 1
fi

# PEP 440-ish release/pre-release only: X.Y.Z, optionally with
# .devN / aN / bN / rcN. No local version segment (+...), no
# anything else. A version that doesn't match this is presumably
# intentional and unusual enough to want a human tagging it by
# hand rather than this automation guessing.
if ! echo "${version}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(\.dev[0-9]+|a[0-9]+|b[0-9]+|rc[0-9]+)?$'; then
echo "::error::version '${version}' is not a recognized release format (expected X.Y.Z, optionally with .devN/aN/bN/rcN, no local metadata); refusing to tag automatically. Tag it manually if this is intentional."
exit 1
fi

tag="v${version}"

if git rev-parse "refs/tags/${tag}" >/dev/null 2>&1; then
echo "Tag ${tag} already exists; nothing to do."
exit 0
fi

if ! grep -qF "[${version}]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no [${version}] heading; refusing to tag an undocumented release. Add the changelog section as part of the version-bump PR."
exit 1
fi

echo "Version ${version} has no matching tag; creating ${tag}."
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "${tag}" -m "Release ${tag}"
git push origin "refs/tags/${tag}"
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
# Linting only against `ruff black mypy` left numpy/pydantic uninstalled,
# so --strict raised disallow_subclassing_any on the BaseModel subclass.
# Consistent with the test / security / docs jobs. CPU torch keeps it lean.
pip install torch --index-url https://download.pytorch.org/whl/cpu
.github/scripts/install_torch_cpu.sh
pip install -e ".[dev]"
- name: Run ruff
run: ruff check .
Expand All @@ -47,7 +47,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
.github/scripts/install_torch_cpu.sh
pip install -e ".[dev]"
- name: Run tests with coverage gate
run: >-
Expand All @@ -69,7 +69,7 @@ jobs:
# packages GROUPOID actually depends on. The previous `safety` step
# installed only the tools and never the project, so it audited the
# wrong environment. CPU torch keeps the install lean.
pip install torch --index-url https://download.pytorch.org/whl/cpu
.github/scripts/install_torch_cpu.sh
pip install -e .
pip install bandit pip-audit
# Pin setuptools LAST. torch requires setuptools<82 and pulls in a
Expand Down Expand Up @@ -114,7 +114,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
.github/scripts/install_torch_cpu.sh
pip install -e ".[dev]"
# Benchmarks run for information only. There is no regression gate:
# timing baselines recorded on one machine are not comparable to a
Expand Down Expand Up @@ -144,7 +144,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
.github/scripts/install_torch_cpu.sh
pip install -e ".[docs]"
- name: Build docs
run: mkdocs build --strict
2 changes: 1 addition & 1 deletion .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
.github/scripts/install_torch_cpu.sh
pip install -e ".[docs]"
- name: Build docs
run: mkdocs build --strict
Expand Down
4 changes: 2 additions & 2 deletions .zenodo.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"upload_type": "software",
"title": "GROUPOID: Groupoid-based aggregation for federated learning on Riemannian manifolds",
"version": "0.1.0.dev3",
"description": "<p>Pre-alpha research prototype exploring groupoid-based aggregation for federated learning on Riemannian manifolds. Implements transport groupoid morphisms, first cohomology (H^1) for consistency detection, the cellular sheaf Laplacian for spectral analysis, parallel transport (Schild's and pole ladders), and Karcher mean aggregation via geomstats.</p><p><strong>Erratum / supersession notice:</strong> this version (v0.1.0.dev3) carries the mathematical corrections introduced in v0.1.0.dev1 over the earlier v0.1.0.dev0 snapshot (version DOI 10.5281/zenodo.20563975): the sheaf (connection) Laplacian is a verified positive-semidefinite operator with a transport-consistent kernel; persistence diagrams retain the homology-dimension label so H0 and H1 are no longer conflated; and H^1 cohomology raises on incomplete cocycles instead of forming partial holonomy. It also carries the v0.1.0.dev2 correction of a first-step magnitude inflation in the RiemannianAdam optimizer's bias initialization (a component not used by the aggregation results), and additionally fixes Morphism str()/format() output to render the compact form instead of a full matrix dump (a display fix; no numerical behavior changes) alongside documentation-accuracy corrections. The v0.1.0.dev0 version DOI must not be cited for results. Cite the concept DOI (10.5281/zenodo.20563974), which always resolves to the latest corrected version, or this version or later.</p>",
"version": "0.1.0.dev4",
"description": "<p>Pre-alpha research prototype exploring groupoid-based aggregation for federated learning on Riemannian manifolds. Implements transport groupoid morphisms, first cohomology (H^1) for consistency detection, the cellular sheaf Laplacian for spectral analysis, parallel transport (Schild's and pole ladders), Riemannian optimizers, and Karcher mean aggregation via geomstats.</p><p><strong>Erratum / supersession notice:</strong> this version (v0.1.0.dev4) carries the mathematical corrections introduced in v0.1.0.dev1 over the earlier v0.1.0.dev0 snapshot (version DOI 10.5281/zenodo.20563975): the sheaf (connection) Laplacian is a verified positive-semidefinite operator with a transport-consistent kernel; persistence diagrams retain the homology-dimension label so H0 and H1 are no longer conflated; and H^1 cohomology raises on incomplete cocycles instead of forming partial holonomy. It also carries the v0.1.0.dev2 correction of a first-step magnitude inflation in the RiemannianAdam optimizer's bias initialization, and the v0.1.0.dev3 Morphism str()/format() display fix and documentation-accuracy corrections (v0.1.0.dev3 was cut in source control but never separately tagged or deposited, so this is the first deposit carrying its changes). New in v0.1.0.dev4: the Riemannian SGD and Adam optimizers now parallel-transport their accumulated moments between iterates instead of projecting them (projection could annihilate a geodesic-aligned moment); the parallel-transport and persistent-homology modules are wired into the aggregation pipeline; and a preregistered synthetic benchmark (see the repository's experiments/ directory) tests the central transport-aggregation hypothesis, finding it supported on synthetic frame-misaligned data and still unvalidated on real federated learning tasks. The v0.1.0.dev0 version DOI must not be cited for results. Cite the concept DOI (10.5281/zenodo.20563974), which always resolves to the latest corrected version, or this version or later.</p>",
"creators": [
{
"name": "Maniches, Santiago",
Expand Down
32 changes: 25 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

Nothing yet.

## [0.1.0.dev4] - 2026-07-06

`v0.1.0.dev3` was cut in source control (below) but never tagged or
released: no `v0.1.0.dev3` tag was ever pushed, so PyPI, the GitHub
Release, and the Zenodo deposit all skip directly from `0.1.0.dev2` to
this version, which carries both the `0.1.0.dev3` changes and the ones
below. See the "Releasing" section of `CONTRIBUTING.md` for the
automation added in this version that prevents this gap from recurring.

### Fixed
- **Optimizer moment accumulators are now parallel-transported between
iterates**: Adam's first moment was previously carried across steps by
Expand All @@ -16,32 +27,38 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
at the new iterate). Both now use the metric's parallel transport (the
Becigneul-Ganea construction), with projection kept as the fallback for
metrics without parallel transport. Regression tests discriminate
transport from projection by exact norm preservation.
transport from projection by exact norm preservation (#49).

### Added
- `TransportGroupoidAggregator.register_transport_from_points`: computes
and registers the transport matrix from two client base points via the
pole or Schild ladder, wiring `groupoid.transport` into the pipeline.
pole or Schild ladder, wiring `groupoid.transport` into the pipeline
(#49).
- Opt-in `track_divergence` flag on the aggregator: each round computes
the persistent-homology summary of the transported parameters with the
H0-vs-H0 bottleneck distance to the previous round, exposed as
`FederatedRound.divergence` (default off; existing behavior unchanged),
wiring `groupoid.persistence` into the pipeline.
wiring `groupoid.persistence` into the pipeline (#49).
- Descent validation for the Riemannian optimizers: SGD, momentum SGD,
and Adam descend to a known target on S^2; the optimizer status labels
in STATUS.md and the docs are upgraded accordingly (general
convergence-rate analysis still does not exist and is not claimed).
convergence-rate analysis still does not exist and is not claimed)
(#49).
- A preregistered synthetic benchmark (`experiments/`): preregistration
pushed before execution, 600 seeded runs, 2x2 transport/mean ablation
against an oracle estimand, corrupted-cocycle conditions, and a
committed preregistered analysis (`experiments/RESULTS.md`). Supports
the transport benefit under frame misalignment (largely by
construction) and the pooled H^1-error correlation under corruption,
with the within-level caveat, a failed null check reported as failed,
and one documented seeding deviation.
and one documented seeding deviation (#49).
- Release automation: pushing a version bump to `main` now tags and
releases automatically, removing the manual tag-push step.

## [0.1.0.dev3] - 2026-07-06

Cut in source control but never tagged or released; see `0.1.0.dev4` above.

### Fixed
- **`Morphism` `str()`/`format()` output**: pydantic's `BaseModel` defines
`__str__` separately from `__repr__`, so `print()`, f-strings, and loguru's
Expand Down Expand Up @@ -137,8 +154,9 @@ Date shown is the tag commit (`v0.1.0.dev0` -> 2a02954). `CITATION.cff` records
`date-released: 2026-05-25`, when the core implementation landed; the tag was
later placed on the metadata commit.

[Unreleased]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev3...HEAD
[0.1.0.dev3]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev2...v0.1.0.dev3
[Unreleased]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev4...HEAD
[0.1.0.dev4]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev2...v0.1.0.dev4
[0.1.0.dev3]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev2...af42cb4
[0.1.0.dev2]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev1...v0.1.0.dev2
[0.1.0.dev1]: https://github.com/smaniches/GROUPOID/compare/v0.1.0.dev0...v0.1.0.dev1
[0.1.0.dev0]: https://github.com/smaniches/GROUPOID/releases/tag/v0.1.0.dev0
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cff-version: 1.2.0
message: "If you use this software, please cite it as below."
type: software
title: "GROUPOID: Groupoid-based aggregation for federated learning on Riemannian manifolds"
version: 0.1.0.dev3
version: 0.1.0.dev4
date-released: 2026-07-06
license: Apache-2.0
repository-code: "https://github.com/smaniches/GROUPOID"
Expand Down
39 changes: 39 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,45 @@ please open an issue using the "Mathematical Correction" template.
6. Ensure CI passes (lint, test, security, docs build).
7. Address all review comments before merging.

## Releasing

1. Open a release-cut PR that bumps the version across every metadata
file that carries it: `pyproject.toml`, `groupoid/__init__.py`,
`codemeta.json`, `CITATION.cff` (version and `date-released`),
`STATUS.md` (both mentions), and `.zenodo.json` (version, the erratum
description, and `isNewVersionOf` advanced to the prior version's
Zenodo DOI). Roll `CHANGELOG.md`'s `Unreleased` section into a new
version heading with compare links. See PRs #40 or #48 for the
template.
2. Merge the PR to `main`.

That's it — merging is the release trigger, split across two workflows
with one job each so the actual publish path has exactly one trigger to
reason about:

- `.github/workflows/auto-tag-release.yml` runs on every push to `main`
that touches `pyproject.toml`. It reads the version out of
`pyproject.toml`, and if no `v<version>` tag exists yet, it creates and
pushes one (as `github-actions[bot]`) after checking the version has a
recognized format and that `CHANGELOG.md` documents it. A push with no
version change, or whose tag already exists, is a no-op.
- That tag push triggers `.github/workflows/release.yml` exactly as a
manual `git tag vX.Y.Z && git push origin vX.Y.Z` would: build,
provenance and SBOM attestation, publish to PyPI via Trusted
Publishing, Sigstore signing, and a GitHub Release (which in turn
triggers the Zenodo deposit for the new version DOI). `release.yml`
itself is unchanged from before this automation existed — it only
ever runs from a tag push or a manual `workflow_dispatch`.

The actual PyPI publish step still runs under the `pypi` GitHub
Environment; if that environment has required reviewers configured, the
publish waits for that approval exactly as before. Automating the tag
does not remove that gate.

If you need to re-publish an existing tag to PyPI only (no new GitHub
Release, no new Zenodo deposit), use the `workflow_dispatch` trigger on
`release.yml` with the `ref` input set to the existing tag.

## Issue Templates

When reporting bugs or requesting features, please use the appropriate
Expand Down
4 changes: 2 additions & 2 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
- No differential privacy mechanism is implemented.
- No formal convergence analysis or proofs exist.
- The package is published on PyPI only as an early development pre-release
(`groupoid 0.1.0.dev3`); no stable release exists yet.
(`groupoid 0.1.0.dev4`); no stable release exists yet.

## Validation status

Expand Down Expand Up @@ -84,5 +84,5 @@ not capture.

## Versioning

This project uses `0.1.0.dev3` to indicate pre-release development.
This project uses `0.1.0.dev4` to indicate pre-release development.
The API is unstable and will change without notice.
2 changes: 1 addition & 1 deletion codemeta.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"codeRepository": "https://github.com/smaniches/GROUPOID",
"programmingLanguage": "Python",
"license": "Apache-2.0",
"version": "0.1.0.dev3",
"version": "0.1.0.dev4",
"author": [
{
"@type": "Person",
Expand Down
2 changes: 1 addition & 1 deletion groupoid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""GROUPOID: Groupoid-based federated learning with Riemannian geometry."""

__version__ = "0.1.0.dev3"
__version__ = "0.1.0.dev4"

from groupoid.aggregation import (
DisconnectedClientGraphError,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "groupoid"
version = "0.1.0.dev3"
version = "0.1.0.dev4"
description = "Research prototype: groupoid-based aggregation for federated learning on Riemannian manifolds"
readme = "README.md"
license = "Apache-2.0"
Expand Down