From 3314d2174f11ed5f4c89b7113a9756e49a60c1fe Mon Sep 17 00:00:00 2001 From: tkclam Date: Thu, 11 Jun 2026 14:28:19 +0200 Subject: [PATCH] docs: restructure into guides/reference and add a MkDocs Material site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganize the documentation into a Diátaxis layout (tutorial / guides / explanation / reference) and publish it as a MkDocs Material site on GitHub Pages, with the library API reference generated from the source docstrings by mkdocstrings. - Trim README to a front page (description, install, common CLI, config intro, links to the site). - New pages: getting-started tutorial, CLI guide, conventions & glossary, complete configuration reference, output-format reference, library API reference. - Move architecture.md -> explanation/pipeline.md (reframed stage by stage), configuration.md / library.md -> guides/ (library folds in the old video.md). - Remove docs/comparison.md and docs/video.md. - Add mkdocs.yml, a `docs` dependency group, and a GitHub Pages deploy workflow. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/docs.yml | 48 ++++ .gitignore | 3 + CONTRIBUTING.md | 18 +- README.md | 89 +++--- docs/architecture.md | 196 ------------- docs/comparison.md | 122 -------- docs/explanation/conventions.md | 113 ++++++++ docs/explanation/pipeline.md | 265 ++++++++++++++++++ docs/getting-started.md | 109 ++++++++ docs/guides/cli.md | 145 ++++++++++ docs/{ => guides}/configuration.md | 170 ++++++------ docs/guides/library.md | 132 +++++++++ docs/index.md | 53 ++++ docs/library.md | 57 ---- docs/reference/api.md | 65 +++++ docs/reference/configuration.md | 305 ++++++++++++++++++++ docs/reference/output-format.md | 106 +++++++ docs/video.md | 35 --- mkdocs.yml | 89 ++++++ pyproject.toml | 9 + uv.lock | 432 +++++++++++++++++++++++++++++ 21 files changed, 2003 insertions(+), 558 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/architecture.md delete mode 100644 docs/comparison.md create mode 100644 docs/explanation/conventions.md create mode 100644 docs/explanation/pipeline.md create mode 100644 docs/getting-started.md create mode 100644 docs/guides/cli.md rename docs/{ => guides}/configuration.md (62%) create mode 100644 docs/guides/library.md create mode 100644 docs/index.md delete mode 100644 docs/library.md create mode 100644 docs/reference/api.md create mode 100644 docs/reference/configuration.md create mode 100644 docs/reference/output-format.md delete mode 100644 docs/video.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..3cc19bd --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,48 @@ +# Build the MkDocs site and publish it to GitHub Pages. +# +# One-time setup: in the repository settings, set Pages -> "Build and deployment" +# -> Source to "GitHub Actions". The site is then served at +# https://nely-epfl.github.io/deeperfly/ and rebuilt on every push to main. +name: docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - "src/**" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress run. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - name: Build site + run: uv run --group docs mkdocs build --strict + - uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 4185513..98bea27 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ deeperfly_outputs*/ # Scratch / working files (not for version control) scratch/ + +# MkDocs build output +site/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00c38fa..59de4e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to deeperfly Development setup. For how the pipeline works, see -[docs/architecture.md](docs/architecture.md). +[docs/explanation/pipeline.md](docs/explanation/pipeline.md). ## Requirements @@ -47,6 +47,22 @@ The hooks run ruff, keep `uv.lock` in sync, and strip notebook outputs with uvx pre-commit run --all-files ``` +## Documentation + +The docs site is built with [MkDocs](https://www.mkdocs.org/) + Material and +lives in [`docs/`](docs) (configured by [`mkdocs.yml`](mkdocs.yml)). The library +API reference is generated from the source docstrings by +[mkdocstrings](https://mkdocstrings.github.io/). Preview it locally with live +reload: + +```bash +uv run --group docs mkdocs serve # http://127.0.0.1:8000 +uv run --group docs mkdocs build --strict # what CI runs +``` + +Pushing to `main` rebuilds and publishes the site to GitHub Pages +(see [`.github/workflows/docs.yml`](.github/workflows/docs.yml)). + ## License By contributing you agree that your contributions are licensed under the diff --git a/README.md b/README.md index 05324da..352ce90 100644 --- a/README.md +++ b/README.md @@ -2,74 +2,55 @@ Markerless 3D pose estimation of tethered *Drosophila* from a multi-camera rig. It estimates camera parameters and 2D/3D keypoint locations from behavioral -recordings: 2D pose → bundle adjustment → triangulation → visualization. +recordings through one linear pipeline: 2D pose → bundle adjustment → +triangulation → visualization. -It is a modern rewrite of -[DeepFly3D](https://github.com/NeLy-EPFL/DeepFly3D), +deeperfly is both a command-line tool and a Python library, and a modern rewrite +of [DeepFly3D](https://github.com/NeLy-EPFL/DeepFly3D), [DeepFly2D](https://github.com/NeLy-EPFL/DeepFly2D) and -[PyBundleAdjustment](https://github.com/semihgunel/PyBundleAdjustment). See [docs/comparison.md](docs/comparison.md) for how it differs -from the originals. +[PyBundleAdjustment](https://github.com/semihgunel/PyBundleAdjustment). + +📖 **[Documentation](https://nely-epfl.github.io/deeperfly/)** ## Installation -deeperfly is both a command-line tool and a Python library. Install the CLI with -[uv](https://docs.astral.sh/uv/): +Install the CLI with [uv](https://docs.astral.sh/uv/) (`--torch-backend=auto` +picks the right PyTorch wheel for your machine): ```bash uv tool install git+https://github.com/NeLy-EPFL/deeperfly --python 3.13 --torch-backend=auto ``` -## Checking your install - -`deeperfly doctor` reports what this machine can run — inference, frame I/O -backends, detector weights, and the default config path. Run it after installing: +As a library in your own project (prefix with `UV_TORCH_BACKEND=auto`, since +`uv add` has no `--torch-backend` flag): ```bash -deeperfly doctor -``` - -The `GPU inference` line under `inference` tells you whether the detector will -run on the GPU: - -``` - GPU inference available (24.0 GiB memory) +UV_TORCH_BACKEND=auto uv add git+https://github.com/NeLy-EPFL/deeperfly ``` -On a CPU-only box it reads `not available -- CPU only`. - -## Quickstart +## Usage ```bash -deeperfly init config.toml # write a config you can edit -deeperfly run recording/ -c config.toml # run the full pipeline -deeperfly inspect recording/deeperfly_outputs/poses.h5 # inspect the result +deeperfly doctor # what this machine can run +deeperfly run recording/ # detect 2D -> 3D -> video +deeperfly inspect recording/deeperfly_outputs/poses.h5 # summarize the result ``` `deeperfly run` does everything in one command: detect 2D pose in every view, -bundle-adjust the cameras, triangulate to 3D, then render a skeleton video. Outputs -land in `recording/deeperfly_outputs/` (override with -`-o`): `poses.h5`, the rendered video, and a copy of the config used. +bundle-adjust the cameras, triangulate to 3D, then render skeleton videos. +Outputs land in `recording/deeperfly_outputs/` (override with `-o`): `poses.h5`, +the rendered videos, and a snapshot of the config used. `deeperfly doctor` +reports whether the detector will run on the GPU and where the weights are +cached. -The config is optional — `deeperfly run recording/` uses sensible defaults. -Generate one with `deeperfly init` to point at your cameras or tweak the -pipeline; the generated file is commented, so edit it in place. See -[docs/configuration.md](docs/configuration.md) for what each section does — from -the `[inputs]` file-to-camera mapping you'll almost always set, through resuming -and partial runs, to the rig and skeleton you can usually leave at their -defaults. +## Configuration -Pass `--log-level debug` for more detail, or `--log-level warning` to quiet the -per-stage logs and progress bar. - -## Library usage - -deeperfly is also a Python library. Add it to your project — prefix with -`UV_TORCH_BACKEND=auto` so uv picks the right PyTorch wheel (`uv add` has no -`--torch-backend` flag): - -```bash -UV_TORCH_BACKEND=auto uv add git+https://github.com/NeLy-EPFL/deeperfly -``` +A run is driven by a single, self-contained `config.toml` — the camera rig, which +file belongs to which camera, the detector, the pipeline and the visualization. +It's optional (`deeperfly run recording/` uses sensible defaults); generate a +commented one to edit with `deeperfly init config.toml` and pass it with `-c`. +Each stage is toggled by a `do_` switch and reuses its cached result when +unchanged, so editing the config recomputes only the affected stages. ```python from deeperfly import CameraGroup, Config, Skeleton, run_from_points2d @@ -79,16 +60,14 @@ result = run_from_points2d(cameras, Skeleton.fly(), pts2d, conf) result.save("fly.h5") ``` -See [docs/library.md](docs/library.md) and the [`examples/`](examples) notebooks -for full walkthroughs. - ## Documentation -- [docs/configuration.md](docs/configuration.md) — the `config.toml`, section by section, ordered by how often you'll edit it. -- [docs/library.md](docs/library.md) — the Python API: bundle adjustment, the pipeline, video I/O. -- [docs/architecture.md](docs/architecture.md) — how the pipeline works: stages, 3D correction (triangulation ransac/greedy/dlt ± pictorial), the detector. -- [docs/video.md](docs/video.md) — frame read/write: PyAV video, OpenCV image sequences (CPU decode). -- [docs/comparison.md](docs/comparison.md) — what changed from DeepFly3D / DeepFly2D / PyBundleAdjustment. +Full docs are at **[nely-epfl.github.io/deeperfly](https://nely-epfl.github.io/deeperfly/)**: + +- [Getting started](https://nely-epfl.github.io/deeperfly/getting-started/) — run the bundled example end to end. +- [CLI usage](https://nely-epfl.github.io/deeperfly/guides/cli/) and [Writing configs](https://nely-epfl.github.io/deeperfly/guides/configuration/). +- [How it works](https://nely-epfl.github.io/deeperfly/explanation/pipeline/) — the pipeline, stage by stage. +- [Library API](https://nely-epfl.github.io/deeperfly/guides/library/) and the complete [reference](https://nely-epfl.github.io/deeperfly/reference/api/). - [CONTRIBUTING.md](CONTRIBUTING.md) — development install, tests, linting. ## License diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 6d99f7e..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,196 +0,0 @@ -# Architecture - -`deeperfly run` is one linear sequence of stages — `pose2d` → -`bundle_adjustment` → `pictorial_structures` (disabled by default) → `triangulation` → -`visualization` — each toggled by a `do_` boolean in `[pipeline]`, with its -own top-level `[]` parameter table. Each stage writes its own group in -`poses.h5`. - -## Data flow - -The two diagrams below show what happens when we run deeperfly on the example dataset with the default config. - -| symbol | meaning | default | -| --- | --- | --- | -| $T$ | total frames | — | -| $V$ | camera views | 7 | -| $H_\text{raw}$, $W_\text{raw}$ | raw frame size (per source) | — | -| $H_\text{in}$, $W_\text{in}$ | network input size | 256 × 512 | -| $H_\text{out}$, $W_\text{out}$ | heatmap size (stride-4 of input) | 64 × 128 | -| $C_\text{out}$ | output channels / heatmaps per model | 19 | -| $P$ | skeleton keypoints (the `P` axis in code) | 38 | -| $C_\text{raw}$, $C_\text{in}$ | RGB channels | 3 | - -### Raw frames → 2D keypoints - -```mermaid -flowchart TD - subgraph SRC["videos"] - direction LR - c0(["camera_0.mp4
(RH)"]) - c1(["camera_1.mp4
(RM)"]) - c2(["camera_2.mp4
(RF)"]) - c3(["camera_3.mp4
(F)"]) - c4(["camera_4.mp4
(LF)"]) - c5(["camera_5.mp4
(LM)"]) - c6(["camera_6.mp4
(LH)"]) - end - - fl["horizontal flip"] - rz["resize, normalize"] - - c0 -->|"raw frames
(T, H_raw, W_raw, C_raw)"| rz - c1 --> rz - c2 --> rz - c3 --> rz - c3 --> fl - c4 -->|"raw frames
(T, H_raw, W_raw, C_raw)"| fl - c5 --> fl - c6 --> fl - - fl -->|"mirrored frames
(T, H_raw, W_raw, C_raw)"| rz - fl --> rz - fl --> rz - fl --> rz - - net["stacked hourglass network"] - rz -->|"preprocessed images
(T, C_in, H_in, W_in)"| net - rz --> net - rz --> net - rz --> net - rz --> net - rz --> net - rz --> net - rz --> net - - am["locate peaks (in original image coordinates)"] - net -->|"keypoint heatmaps
(T, C_out, H_out, W_out)"| am - net --> am - net --> am - net --> am - net --> am - net --> am - net --> am - net --> am - - sc["Route channel c in path i to point p in view v"] - am -->|"peak locations (T, C_out, 2),
confidence (T, C_out)"| sc - am --> sc - am --> sc - am --> sc - am --> sc - am --> sc - am --> sc - am --> sc - - sc --> out(["2D keypoints
(V, T, P, 2)"]) - sc --> out2(["confidence
(V, T, P)"]) - - %% one color per pathway (skeleton limb_palette): right=reds, left=blues, - %% front camera's two lanes in the right/left antenna shades. - linkStyle 0,12,20,28 stroke:#d58579,stroke-width:2px - linkStyle 1,13,21,29 stroke:#c9564f,stroke-width:2px - linkStyle 2,14,22,30 stroke:#ba1e31,stroke-width:2px - linkStyle 3,15,23,31 stroke:#8c1525,stroke-width:2px - linkStyle 4,8,16,24,32 stroke:#0a4f6b,stroke-width:2px - linkStyle 5,9,17,25,33 stroke:#0f7399,stroke-width:2px - linkStyle 6,10,18,26,34 stroke:#1a8daf,stroke-width:2px - linkStyle 7,11,19,27,35 stroke:#75becb,stroke-width:2px -``` - -The hourglass network was trained to output 19 heatmaps which correspond to the 19 keypoints on the right side of the fly's body. Therefore, the left cameras are mirrored to give the -detector a "right-looking" fly. The front camera (`camera_3`) feeds *two* lanes — un-flipped for -the keypoints on the right, mirrored for the left. - -### 2D keypoints → 3D keypoints - -```mermaid -flowchart TD - kp2d(["2D keypoints (V, T, P, 2),
with unobserved = NaN"]) - conf(["confidence (V, T, P)"]) - cam0(["initial camera parameters
(intrinsics + extrinsics),
7 cameras"]) - - ba["bundle adjustment"] - - kp2d --> ba - conf -.-> ba - cam0 --> ba - - tri["triangulation"] - - ba -->|"refined camera parameters"| tri - kp2d --> tri - conf -.-> tri - - tri --> res(["3D keypoints
(T, P, 3)"]) -``` - -## Pipeline stages - -| Stage | Module | Notes | -| --- | --- | --- | -| 2D pose | `pose2d/` (`model.py`, `weights.py`) | Stacked hourglass (PyTorch) running the original DeepFly2D weights directly; CUDA / Metal automatically. | -| Bundle adjustment | `pipeline.bundle_adjust_cameras` → `bundle_adjustment/` | Fly-as-target BA: confidence weights, Huber loss, bone-length prior; frames subsampled by `max_frames` / `frame_sampling` (`even`/`confidence`/`coverage`/`diversity`). | -| Triangulation | `triangulation.py` / `pipeline.reconstruct{,_ransac}` | NaN-aware DLT: RANSAC consensus (default), greedy reprojection-outlier rejection, or plain DLT, optionally after pictorial-structures peak recovery (`pictorial.py`). | -| Visualization | `visualization/`, `io/` | OpenCV 2D overlays + reprojected 3D skeleton, composited to MP4. | -| Result I/O | `results.py` | Self-contained HDF5 `PoseResult`. | -| Skeleton | `skeleton.py` (default in `data/default_config.toml`) | 38 points, 10 limbs, 28 bones, per-camera visibility. | - -## 3D correction: triangulation (± pictorial) - -Each view is detected independently; the views only meet *geometrically*. The -reconstruction is two orthogonal choices — `run_from_points2d(..., -triangulation=..., do_pictorial=...)` for the library, or -`[triangulation].method` + `[pipeline].do_pictorial_structures` for the -CLI: - -**`triangulation`** — how the per-view 2D points become one 3D point: - -- **`ransac`** (default) — triangulate each point from its largest set of - mutually consistent views, *vetoing* a bad detection. The rig has only a handful - of cameras, so it exhaustively enumerates all `C(V,2)` two-view hypotheses (the - deterministic limit of RANSAC), counts inliers within `ransac_threshold` px, - breaks ties toward lower total reprojection error, and refits from the inliers. - A gross outlier never enters the fit; NaN views never count as inliers. -- **`greedy`** — triangulate the arg-max detections by DLT and iteratively drop - the single worst-reprojecting view of each offending point, re-triangulating - from the survivors (`reproj_threshold` / `max_drops`). Cheaper, but refines an - already-contaminated fit. -- **`dlt`** — plain least-squares triangulation, no outlier handling. - -**`do_pictorial_structures`** (default off; `do_pictorial=` in the library call) — -when on, first run DeepFly3D-style pictorial structures over the detector's top-K -candidate peaks (`pictorial.py`): build multi-view-consistent 3D hypotheses per -joint, then pick one per joint by exact dynamic programming along each limb under -bone-length priors (plus an optional temporal term). It can *recover* a joint when -the arg-max landed on the wrong heatmap peak (occlusion, crossing legs, L/R -confusion) — something the triangulators can only *veto*. It needs the -full-heatmap detect path (slower); its committed per-view 2D then feeds the chosen -`triangulation` (a plain `dlt` pass keeps the PS estimate). On clean recordings -it is a no-op. - -## 2D detector - -The detector is a faithful PyTorch copy of the original DeepFly2D stacked -hourglass: `pose2d/model.py` (`HourglassNet`, `predict_heatmaps`) and -`pose2d/weights.py` (`load_model`), behind the torch-free `pose2d/detector.py` -seam. It loads the published `sh8` weights directly, with no conversion; -`deeperfly run` downloads them on first use. -`pose2d/inference.py` preprocesses frames in torch, so a GPU-decoded frame is -normalized, resized and forwarded without leaving the GPU. - -The detector uses CUDA automatically on NVIDIA and Metal (MPS) on Apple Silicon, -with no setup. For large CUDA batches the forward is wrapped with `torch.compile` -(see `pose2d/model.py`). Geometry and bundle adjustment are the -only JAX in deeperfly and run in float64 on the CPU. - -## Caching and re-runs - -Each stage records the config subset that produced it in `/run.json` (a -*fingerprint*). On a re-run an enabled stage is reused while its fingerprint -still matches and its output is present; it recomputes when its parameters -changed, its output is missing, `--overwrite` selects it, or an upstream stage -recomputed (the cascade). Performance-only knobs (`batch_size`, `decode_buffer`, -`[io.image]`) never invalidate a cache. The `pose2d` cache always feeds -downstream (so `do_pose2d = false` reconstructs from a stored 2D pose); a -*derived* stage's output feeds downstream only while that stage is enabled. diff --git a/docs/comparison.md b/docs/comparison.md deleted file mode 100644 index a844b87..0000000 --- a/docs/comparison.md +++ /dev/null @@ -1,122 +0,0 @@ -# deeperfly vs. DeepFly3D / DeepFly2D / PyBundleAdjustment - -`deeperfly` is a clean-room, JAX-first re-implementation of the markerless -*Drosophila* 3D-pose pipeline from three NeLy-EPFL projects: - -| Upstream project | Role | `deeperfly` counterpart | -| --- | --- | --- | -| [DeepFly2D](https://github.com/NeLy-EPFL/DeepFly2D) | 2D pose: a PyTorch stacked-hourglass detector | [`pose2d/`](../src/deeperfly/pose2d) — a faithful PyTorch port running the original weights directly | -| [DeepFly3D](https://github.com/NeLy-EPFL/DeepFly3D) | The orchestrating 2D→3D pipeline + GUI ([Günel et al., *eLife* 2019](https://doi.org/10.7554/eLife.48571)) | [`pipeline/`](../src/deeperfly/pipeline), [`triangulation.py`](../src/deeperfly/triangulation.py), [`pictorial.py`](../src/deeperfly/pictorial.py) | -| [PyBundleAdjustment](https://github.com/semihgunel/PyBundleAdjustment) | scipy-based bundle adjustment | [`bundle_adjustment/`](../src/deeperfly/bundle_adjustment) | - -The science is faithful to the originals — same camera rig, same detector -weights, same fly-as-target bundle adjustment, same pictorial- -structures idea. What changed is the implementation: everything geometric is -JAX (JIT- and autodiff-friendly), the whole thing is headless and scriptable, -and the I/O is modern. - -## At a glance - -| | DeepFly3D / DeepFly2D / PyBundleAdjustment | deeperfly | -| --- | --- | --- | -| Numerical core | NumPy + SciPy + PyTorch | **JAX** (float64 geometry/BA), PyTorch for the detector | -| 2D detector | Stacked hourglass (PyTorch) | Same network + weights, a faithful **PyTorch** port running the released weights directly | -| Bundle-adjustment Jacobian | SciPy `least_squares`, sparse | SciPy `least_squares` with an **analytic JAX Jacobian** + sparsity pattern | -| 3D correction | Pictorial structures (belief propagation) | **Triangulation** — RANSAC consensus (default), greedy reprojection-outlier rejection, or plain DLT — optionally after a re-implemented pictorial-structures corrector (exact DP) | -| Interface | PyQt **GUI** | **Headless CLI + library** (one merged `config.toml`) | -| Result I/O | Pickle / custom | Self-contained **HDF5** (`PoseResult`) | -| Acceleration | CUDA (PyTorch) | CUDA (PyTorch detector) and Apple **Metal/MPS** | -| Scope | Training + inference + GUI correction | **Inference only** (uses the published weights), headless | - -## Component-by-component - -### 2D detector - -Both use the same stacked-hourglass architecture and the same published `sh8` -weights. deeperfly is a faithful **PyTorch** port that loads the released weights -directly (no conversion), batches the seven views through the network in one call, -and downloads the weights on first use. It runs on CUDA (NVIDIA) and Metal/MPS -(Apple Silicon) automatically. - -deeperfly does **not** include training code — it consumes the released weights. -Train or fine-tune with the upstream DeepFly2D repository. - -### Bundle adjustment - -Like the originals, deeperfly runs bundle adjustment with **no external target**: -the fly's own detected joints are the points it refines -(`pipeline.bundle_adjust_cameras`). The solver is still SciPy's `least_squares` (TRF + LSMR), -but the per-observation residual and its **Jacobian are computed analytically in -JAX** (`jax.vmap` + `jax.jacfwd`) and assembled into a sparse matrix from a -precomputed sparsity pattern. - -Beyond PyBundleAdjustment, the `bundle_adjustment/` module adds: - -- a declarative **fixed/shared parameter grammar** (e.g. `"*.intr"`, - `"f.rvec"`, tying `[["lf.tvec[2]", "rf.tvec[2]"]]`) to anchor the gauge; -- per-observation **confidence weighting** from detector heatmaps; -- a robust **Huber** loss and an optional **bone-length prior**. - -### Triangulation - -Both triangulate by DLT. deeperfly's is **NaN-aware** (a `NaN` observation means -"this view can't see this point", so visibility needs no separate mask array) -and returns `NaN` for points seen by fewer than two views. On top, the -`triangulation` choice offers a **RANSAC consensus** triangulator (default — -exhaustive `C(V,2)` two-view hypotheses, inlier counting with error tie-breaking), -a greedy **reprojection-outlier rejection** pass, or **plain DLT**. - -### 3D correction - -DeepFly3D corrects erroneous 2D detections with **pictorial structures** — -belief propagation over candidate joint locations under learned bone-length -priors and multi-view geometry. deeperfly splits this into two orthogonal knobs: -a `triangulation` strategy that *vetoes* bad views, and an optional `pictorial` -stage that *recovers* the right peak first. - -- **`triangulation`** (`[triangulation].method`): - - **`ransac`** (default) — triangulate each point from its largest multi-view - consensus set; a gross outlier never enters the fit. - - **`greedy`** — greedily drop the worst-reprojecting view of each offending - point. Cheaper; refines a (possibly contaminated) least-squares fit. - - **`dlt`** — plain least-squares triangulation, no outlier handling. -- **`do_pictorial_structures`** (`[pipeline].do_pictorial_structures`) — a re-implementation of the - DeepFly3D idea over the top-K candidate peaks. Because the fly skeleton's bones - form a forest of simple chains (each leg a 5-joint path), the MAP estimate is - solved by **exact dynamic programming** per limb — no loopy belief propagation — - with an optional temporal term. It can *recover* a joint when the arg-max landed - on the wrong peak (occlusion, crossing legs, L/R confusion); its committed 2D - then feeds the chosen `triangulation` (a plain `dlt` pass keeps the PS estimate). - -### Interface, I/O, reproducibility - -DeepFly3D ships a PyQt **GUI** for visualization and manual correction. -deeperfly is **headless**: a single `deeperfly run` drives the whole pipeline -from one merged `config.toml`, with per-stage caching so re-runs only compute -what changed. Results are a self-contained **HDF5** `PoseResult` instead of -pickled objects, and visualization is OpenCV overlays composited to MP4. - -### Performance - -The geometry/BA core is JAX with float64, so projection, triangulation and the BA -residual/Jacobian are JIT-compiled and vectorized. Detection batches all views -through the network and **streams** frames in fixed-size windows (constant memory -for arbitrarily long recordings), decoding on the CPU and uploading each window to -the detector in one shot — decode is not the bottleneck. The detector uses CUDA -(NVIDIA) and **Metal/MPS** (Apple Silicon) automatically. - -## What deeperfly intentionally drops - -- **No GUI** and no manual point-by-point correction — it is built to run in a - script or on a cluster. -- **No training** — it runs the published detector weights. -- **No legacy formats** — HDF5 only, no pickle importer. - -## References - -- P. Günel, H. Rhodin, D. Morales, J. Campagnolo, P. Ramdya, P. Fua. - *DeepFly3D, a deep learning-based approach for 3D limb and appendage tracking - in tethered, adult Drosophila.* eLife 8:e48571 (2019). -- [DeepFly3D](https://github.com/NeLy-EPFL/DeepFly3D) · - [DeepFly2D](https://github.com/NeLy-EPFL/DeepFly2D) · - [PyBundleAdjustment](https://github.com/semihgunel/PyBundleAdjustment) diff --git a/docs/explanation/conventions.md b/docs/explanation/conventions.md new file mode 100644 index 0000000..7143a7a --- /dev/null +++ b/docs/explanation/conventions.md @@ -0,0 +1,113 @@ +# Conventions & glossary + +The conventions every part of deeperfly shares — array layouts, the missing-data +encoding, and the coordinate frames — plus a glossary of the terms used across +the docs and the config. + +## Array layouts + +Arrays are **view-leading**: the camera/view axis comes first. + +| Array | Shape | Meaning | +| --- | --- | --- | +| `pts2d` | `(V, T, P, 2)` | 2D keypoints: per view, per frame, per point, `(x, y)` in raw-frame pixels. | +| `conf` | `(V, T, P)` | Detector confidence for each 2D observation. | +| `pts3d` | `(T, P, 3)` | 3D keypoints: per frame, per point, `(x, y, z)` in world units. | +| `reproj_error` | `(V, T, P)` | Per-view reprojection error of the 3D point, in pixels. | + +The axes are referred to throughout by these letters: + +- **`V`** — camera **views** (7 in the default rig). +- **`T`** — **frames** (time). +- **`P`** — skeleton **points** / keypoints (38 in the default skeleton). + +Single-image helpers (e.g. `CameraGroup.project`) drop the `T` axis and use +`(V, N, 2)` / `(N, 3)`, where `N` is the number of points. + +## NaN means missing + +There is no separate visibility mask. A keypoint that a view does not observe is +stored as `NaN`, and the same convention carries through: + +- The detector's `[pose2d.output_points]` scatter leaves an unfilled + `(view, point)` as `NaN` — the union of the per-view tables *is* the visibility. +- Triangulation ignores `NaN` views and returns `NaN` for a point seen by fewer + than `min_inliers` views. +- The float64 HDF5 datasets preserve `NaN`, so it round-trips through `poses.h5`. + +When you read `pts3d`, treat `NaN` as "not reconstructed for this frame/point". +Use `np.nanmedian` / `np.nanmax` and friends, as `deeperfly inspect` does. + +## Coordinate frames + +- **Pixels** are in the **raw source frame** that a view's intrinsics describe. + Any per-pathway preprocessing (flip, crop, resize) is *inverted* before the + points are stored, so a mirror fed to the detector never moves the stored 2D or + the reconstructed 3D. +- **World units** are whatever the rig's `distance` / intrinsics imply (the + default rig is metric-like but unitless). World **up** is `+z`. +- **Cameras** use the orbit (look-at) parameterization in the config: + `look_at`, `distance`, `azimuth_deg`, `elevation_deg`, `roll_deg`. Internally a + camera is the usual `rvec` (Rodrigues rotation), `tvec`, intrinsics + `[fx, fy, cx, cy]`, and OpenCV-ordered distortion coefficients. + +## Numerics + +The geometry core — projection, triangulation, and the bundle-adjustment +residual and Jacobian — is **JAX in float64 on the CPU**; the arrays are tiny, so +a GPU never helps. The **2D detector is PyTorch** and uses the GPU (CUDA or +Metal/MPS) automatically. Detector forward precision is configurable +(`[pose2d].precision`), but everything geometric stays float64. + +## Confidence + +`conf` is the detector's heatmap-peak confidence for each 2D observation. +`weigh_by_confidence` (in `[bundle_adjustment]` and `[triangulation]`) scales each +observation's least-squares contribution by `sqrt(confidence)`, so surer +detections pull harder; non-positive or non-finite confidences drop the +observation. For RANSAC the weighting affects the candidate fits and the final +refit but not the inlier vote, which stays a pure geometric reprojection test. + +## Glossary + +**Source** — a named footage glob (`[[sources]]`), decoded once. Decoupled from +cameras and pathways, which reference it by name, so one source can feed several +pathways. + +**Pathway** — one `source → preprocessor → model` inference run +(`[[pose2d.pathways]]`). It says *what to detect on*; where its outputs land is in +`[pose2d.output_points]`. + +**Preprocessor** — a named, reusable list of frame ops (flip/crop/rotate/resize) +applied to a pathway's frames before the model (`[[pose2d.preprocessors]]`). + +**Model** — a detector network plus its weights and input contract +(`[[pose2d.models]]`); `class = "hourglass"` is the DeepFly2D stacked hourglass. + +**Detection plan** — the parsed whole of `[[sources]]` + the `[pose2d]` +sub-tables: the mapping of footage through pathways into the skeleton's per-view +2D points. + +**View / camera** — a geometric camera in the rig (`[cameras.]`): pure +intrinsics + extrinsics. A pathway maps its 2D points back into a view's raw +frame. + +**Rig / `CameraGroup`** — the set of named cameras as one object. + +**Skeleton** — the tracked points and their structure (`[skeleton]`): +`point_names`, the `limb_points` kinematic chains, and the plotting palette. + +**Limb** — a named chain of points (e.g. a 5-joint leg) used for the bone-length +prior and for drawing. + +**Candidates** — the detector's top-`k` heatmap peaks per joint, cached by +`pose2d` when `pictorial_structures` is enabled; the input the peak-recovery stage +reconsiders. + +**Stage** — one step of the linear pipeline (`pose2d`, `bundle_adjustment`, +`pictorial_structures`, `triangulation`, `visualization`), toggled by +`[pipeline].do_` and configured by its `[]` table. + +**Fingerprint** — the result-affecting config subset recorded per stage in +`run.json`; a stage's cache is reused only while its fingerprint still matches +(see [caching](pipeline.md#caching-and-re-runs)). diff --git a/docs/explanation/pipeline.md b/docs/explanation/pipeline.md new file mode 100644 index 0000000..5c67718 --- /dev/null +++ b/docs/explanation/pipeline.md @@ -0,0 +1,265 @@ +# How deeperfly works + +`deeperfly run` is one linear sequence of stages — `pose2d` → +`bundle_adjustment` → `pictorial_structures` (off by default) → `triangulation` +→ `visualization`. Each stage is toggled by a `do_` boolean in +`[pipeline]`, configured by its own top-level `[]` table, and (except +`visualization`) writes its own group in `poses.h5`. This page walks through what +each stage consumes and produces; the cross-cutting array layouts and terms it +uses are collected in [Conventions & glossary](conventions.md). + +## Data flow + +The two diagrams below show what happens when we run deeperfly on the example +dataset with the default config. + +| symbol | meaning | default | +| --- | --- | --- | +| $T$ | total frames | — | +| $V$ | camera views | 7 | +| $H_\text{raw}$, $W_\text{raw}$ | raw frame size (per source) | — | +| $H_\text{in}$, $W_\text{in}$ | network input size | 256 × 512 | +| $H_\text{out}$, $W_\text{out}$ | heatmap size (stride-4 of input) | 64 × 128 | +| $C_\text{out}$ | output channels / heatmaps per model | 19 | +| $P$ | skeleton keypoints (the `P` axis in code) | 38 | +| $C_\text{raw}$, $C_\text{in}$ | RGB channels | 3 | + +### Raw frames → 2D keypoints + +```mermaid +flowchart TD + subgraph SRC["videos"] + direction LR + c0(["camera_0.mp4
(RH)"]) + c1(["camera_1.mp4
(RM)"]) + c2(["camera_2.mp4
(RF)"]) + c3(["camera_3.mp4
(F)"]) + c4(["camera_4.mp4
(LF)"]) + c5(["camera_5.mp4
(LM)"]) + c6(["camera_6.mp4
(LH)"]) + end + + fl["horizontal flip"] + rz["resize, normalize"] + + c0 -->|"raw frames
(T, H_raw, W_raw, C_raw)"| rz + c1 --> rz + c2 --> rz + c3 --> rz + c3 --> fl + c4 -->|"raw frames
(T, H_raw, W_raw, C_raw)"| fl + c5 --> fl + c6 --> fl + + fl -->|"mirrored frames
(T, H_raw, W_raw, C_raw)"| rz + fl --> rz + fl --> rz + fl --> rz + + net["stacked hourglass network"] + rz -->|"preprocessed images
(T, C_in, H_in, W_in)"| net + rz --> net + rz --> net + rz --> net + rz --> net + rz --> net + rz --> net + rz --> net + + am["locate peaks (in original image coordinates)"] + net -->|"keypoint heatmaps
(T, C_out, H_out, W_out)"| am + net --> am + net --> am + net --> am + net --> am + net --> am + net --> am + net --> am + + sc["Route channel c in path i to point p in view v"] + am -->|"peak locations (T, C_out, 2),
confidence (T, C_out)"| sc + am --> sc + am --> sc + am --> sc + am --> sc + am --> sc + am --> sc + am --> sc + + sc --> out(["2D keypoints
(V, T, P, 2)"]) + sc --> out2(["confidence
(V, T, P)"]) + + %% one color per pathway (skeleton limb_palette): right=reds, left=blues, + %% front camera's two lanes in the right/left antenna shades. + linkStyle 0,12,20,28 stroke:#d58579,stroke-width:2px + linkStyle 1,13,21,29 stroke:#c9564f,stroke-width:2px + linkStyle 2,14,22,30 stroke:#ba1e31,stroke-width:2px + linkStyle 3,15,23,31 stroke:#8c1525,stroke-width:2px + linkStyle 4,8,16,24,32 stroke:#0a4f6b,stroke-width:2px + linkStyle 5,9,17,25,33 stroke:#0f7399,stroke-width:2px + linkStyle 6,10,18,26,34 stroke:#1a8daf,stroke-width:2px + linkStyle 7,11,19,27,35 stroke:#75becb,stroke-width:2px +``` + +The hourglass network was trained to output 19 heatmaps which correspond to the +19 keypoints on the right side of the fly's body. Therefore, the left cameras +are mirrored to give the detector a "right-looking" fly. The front camera +(`camera_3`) feeds *two* lanes — un-flipped for the keypoints on the right, +mirrored for the left. + +### 2D keypoints → 3D keypoints + +```mermaid +flowchart TD + kp2d(["2D keypoints (V, T, P, 2),
with unobserved = NaN"]) + conf(["confidence (V, T, P)"]) + cam0(["initial camera parameters
(intrinsics + extrinsics),
7 cameras"]) + + ba["bundle adjustment"] + + kp2d --> ba + conf -.-> ba + cam0 --> ba + + tri["triangulation"] + + ba -->|"refined camera parameters"| tri + kp2d --> tri + conf -.-> tri + + tri --> res(["3D keypoints
(T, P, 3)"]) +``` + +## The stages, one at a time + +### 1. `pose2d` — 2D detection + +- **Consumes:** the recording's footage (the `[[sources]]` globs), plus the + detection plan (`[[pose2d.preprocessors]]` / `[[pose2d.models]]` / + `[[pose2d.pathways]]` / `[pose2d.output_points]`). +- **Produces:** `pts2d` `(V, T, P, 2)` and `conf` `(V, T, P)`, the config camera + rig as built at detect time, the raw image sizes, and — when + `pictorial_structures` is enabled — the detector's top-K candidate peaks. +- **Cached in:** `pose2d/` (the whole `poses.h5` is rewritten when this stage + runs, since everything downstream derives from it). + +Each pathway runs its source's frames (optionally preprocessed, e.g. mirrored) +through a stacked-hourglass network, locates the heatmap peaks, maps them back +into the raw source frame, and `[pose2d.output_points]` scatters each output +channel into its `(view, point)` slot. A `(view, point)` no pathway fills is +left `NaN` — that union *is* the visibility, with no separate mask. Frames are +streamed in fixed-size windows, so memory is constant regardless of clip length. + +### 2. `bundle_adjustment` — refine the cameras + +- **Consumes:** the config rig and the 2D detections (`pts2d`, `conf`), plus the + skeleton for the bone-length prior. +- **Produces:** a refined `CameraGroup`. +- **Cached in:** `bundle_adjustment/cameras/`. + +Bundle adjustment uses the fly itself as the calibration target — no external +checkerboard. It refines the camera intrinsics/extrinsics so the rig's +reprojections best agree with the detected joints, subsampling frames +(`max_frames` / `frame_sampling`) and anchoring the world gauge with the +`fixed` / `shared` parameter grammar. The solver is `scipy.optimize.least_squares` +with an analytic JAX Jacobian. + +### 3. `pictorial_structures` — peak recovery (opt-in) + +- **Consumes:** the cached top-K candidate peaks from `pose2d`, the skeleton, and + the rig (BA-refined if available, else the config rig). +- **Produces:** PS-corrected `pts2d`, an initial `pts3d`, and `reproj_error`. +- **Cached in:** `pictorial_structures/`. + +Off by default. When on, it reconsiders the detector's *alternative* peaks per +joint and picks the multi-view-consistent configuration under bone-length priors +— recovering a joint when the arg-max landed on the wrong peak (occlusion, +crossing legs, L/R confusion). Because it needs the candidate peaks, enabling it +re-runs `pose2d` once to extract them. See the [reconstruction +deep-dive](#3d-reconstruction-triangulation-pictorial) below. + +### 4. `triangulation` — 2D → 3D + +- **Consumes:** 2D points (`pictorial_structures`-corrected if that stage ran, + else pristine `pose2d`), the rig (BA-refined if available, else config), and + optionally `conf`. +- **Produces:** `pts3d` `(T, P, 3)`, cleaned `pts2d`, and `reproj_error`. +- **Cached in:** `triangulation/`. + +Lifts the per-view 2D observations into one 3D point per joint per frame by +multi-view geometry. The `method` (`ransac` / `greedy` / `dlt`) chooses how +outliers are handled — see below. + +### 5. `visualization` — render videos + +- **Consumes:** the assembled result (best 2D + 3D from the enabled stages, the + rig, the skeleton) and the footage for `imshow` panels. +- **Produces:** one MP4 per `[[visualization.videos]]` entry under `/`. +- **Cached:** keeps no `poses.h5` group; reuse is keyed on the rendered MP4s + existing and the video specs being unchanged. + +Each video is composited panel by panel (OpenCV overlays for 2D, a depth-sorted +reprojected skeleton for 3D) and streamed to an H.264 MP4 via PyAV, so a long +clip is never held in memory. + +## 3D reconstruction: triangulation (± pictorial) + +Each view is detected independently; the views only meet *geometrically*. The +reconstruction is two orthogonal choices — `run_from_points2d(..., +triangulation=..., do_pictorial=...)` for the library, or +`[triangulation].method` + `[pipeline].do_pictorial_structures` for the CLI: + +**`triangulation`** — how the per-view 2D points become one 3D point: + +- **`ransac`** (default) — triangulate each point from its largest set of + mutually consistent views, *vetoing* a bad detection. The rig has only a + handful of cameras, so it exhaustively enumerates all `C(V,2)` two-view + hypotheses (the deterministic limit of RANSAC), counts inliers within + `ransac_threshold` px, breaks ties toward lower total reprojection error, and + refits from the inliers. A gross outlier never enters the fit; NaN views never + count as inliers. +- **`greedy`** — triangulate the arg-max detections by DLT and iteratively drop + the single worst-reprojecting view of each offending point, re-triangulating + from the survivors (`reproj_threshold` / `max_drops`). Cheaper, but refines an + already-contaminated fit. +- **`dlt`** — plain least-squares triangulation, no outlier handling. + +**`do_pictorial_structures`** (default off; `do_pictorial=` in the library call) +— when on, first run DeepFly3D-style pictorial structures over the detector's +top-K candidate peaks: build multi-view-consistent 3D hypotheses per joint, then +pick one per joint by exact dynamic programming along each limb under bone-length +priors (plus an optional temporal term). It can *recover* a joint when the +arg-max landed on the wrong heatmap peak — something the triangulators can only +*veto*. It needs the full-heatmap detect path (slower); its committed per-view 2D +then feeds the chosen `triangulation` (a plain `dlt` pass keeps the PS estimate). +On clean recordings it is a no-op. + +## The 2D detector + +The detector is a faithful PyTorch copy of the original DeepFly2D stacked +hourglass (`pose2d/model.py`, `pose2d/weights.py`), behind the torch-free +`pose2d/detector.py` seam. It loads the published `sh8` weights directly, with +no conversion; `deeperfly run` downloads them on first use. +`pose2d/inference.py` preprocesses frames in torch, so a GPU-decoded frame is +normalized, resized and forwarded without leaving the GPU. + +The detector uses CUDA automatically on NVIDIA and Metal (MPS) on Apple Silicon, +with no setup. For large CUDA batches the forward is wrapped with `torch.compile`. +Geometry and bundle adjustment are the only JAX in deeperfly and run in float64 +on the CPU. + +## Caching and re-runs + +Each stage records the config subset that produced it in `/run.json` (a +*fingerprint*). On a re-run an enabled stage is reused while its fingerprint +still matches and its output is present; it recomputes when its parameters +changed, its output is missing, `--overwrite` selects it, or an upstream stage +recomputed (the cascade). Performance-only knobs (`batch_size`, `decode_buffer`, +`[io.image]`) never invalidate a cache. The `pose2d` cache always feeds +downstream (so `do_pose2d = false` reconstructs from a stored 2D pose); a +*derived* stage's output feeds downstream only while that stage is enabled. + +For the resume/recompute workflow from the command line see the +[CLI guide](../guides/cli.md#resuming-and-recomputing); for the exact +`run.json` / `poses.h5` layout see the +[output-format reference](../reference/output-format.md). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..3594e74 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,109 @@ +# Getting started + +This walkthrough takes you from a clean machine to a rendered 3D pose video using +the recording bundled with the repository. It should take a few minutes plus the +one-time detector-weight download. + +## 1. Install + +Install the CLI with [uv](https://docs.astral.sh/uv/). `--torch-backend=auto` +lets uv pick the right PyTorch wheel for your machine (CUDA, Metal, or CPU): + +```bash +uv tool install git+https://github.com/NeLy-EPFL/deeperfly --python 3.13 --torch-backend=auto +``` + +To follow along with the bundled example, clone the repo too (the example +footage lives under `examples/data/`): + +```bash +git clone https://github.com/NeLy-EPFL/deeperfly +cd deeperfly +``` + +## 2. Check the install + +`deeperfly doctor` reports what this machine can run — accelerators, frame-I/O +backends, the detector weights, and the default config path: + +```bash +deeperfly doctor +``` + +The `GPU inference` line tells you whether the detector will use the GPU: + +``` + GPU inference available (24.0 GiB memory) +``` + +On a CPU-only box it reads `not available -- CPU only`. deeperfly still runs on +CPU — just slower. (If you installed as a tool rather than cloning, prefix the +commands below with nothing; inside a cloned checkout you can instead use +`uv run deeperfly ...`.) + +## 3. Run the pipeline + +The example recording is the standard 7-camera rig (`camera_0.mp4` … +`camera_6.mp4`), which the packaged default config already targets — so you can +run it with no `-c`: + +```bash +deeperfly run examples/data/ +``` + +This detects 2D pose in every view, bundle-adjusts the cameras, triangulates to +3D, and renders the skeleton videos. The first run downloads the detector weights +and (on CUDA) spends a little time on `torch.compile`; later runs skip both. + +Outputs land in `examples/data/deeperfly_outputs/` (override with `-o`): + +``` +examples/data/deeperfly_outputs/ +├── poses.h5 # cameras, skeleton, 2D + 3D keypoints, reprojection error +├── config.toml # a snapshot of the exact config this run used +├── run.json # per-stage fingerprints (drives cache reuse) +├── pose2d.mp4 # camera montage with the 2D detections drawn on +└── pose3d.mp4 # same montage with the triangulated 3D skeleton reprojected +``` + +## 4. Inspect the result + +```bash +deeperfly inspect examples/data/deeperfly_outputs/poses.h5 +``` + +``` +file: examples/data/deeperfly_outputs/poses.h5 +views: 7 ['rh', 'rm', 'rf', 'f', 'lf', 'lm', 'lh'] +frames: 100 +skeleton: fly38 (38 points) +has 3D: True +reproj: median 2.1 px max 8.7 px +``` + +A low median reprojection error means the cameras and 3D points agree well across +views. Open `pose3d.mp4` to see the reconstructed skeleton. + +## 5. Re-run after a tweak + +Re-running a finished recording is a cheap no-op — every stage's cache is reused. +**Editing the config recomputes only the affected stages.** Generate an editable +config, change something cheap (say the videos), and re-run: + +```bash +deeperfly init config.toml # a fully commented config +# ...edit [visualization] or [triangulation]... +deeperfly run examples/data/ -c config.toml # only the changed stages recompute +``` + +The slow 2D detection is reused; only triangulation/visualization rebuild. That +resume/recompute model — and `--overwrite` to force a redo — is the subject of +the [CLI guide](guides/cli.md#resuming-and-recomputing). + +## Where to go next + +- **[CLI usage](guides/cli.md)** — batch runs, output layout, every flag. +- **[Writing configs](guides/configuration.md)** — point deeperfly at your own + cameras and tune the pipeline. +- **[How it works](explanation/pipeline.md)** — what each stage actually does. +- **[Library API](guides/library.md)** — drive the pipeline from Python. diff --git a/docs/guides/cli.md b/docs/guides/cli.md new file mode 100644 index 0000000..07447e3 --- /dev/null +++ b/docs/guides/cli.md @@ -0,0 +1,145 @@ +# CLI usage + +`deeperfly` has four commands: `init` (write a config), `run` (the pipeline), +`inspect` (summarize a result), and `doctor` (report the install). Every command +takes `--log-level` (`debug` / `info` / `warning` / `error` / `critical`; +`warning` or higher hides the per-stage logs and the progress bar) and `-h` / +`--help`. + +```bash +deeperfly --help # the command list +deeperfly run --help # a command's options +``` + +## `deeperfly init` — write a config + +```bash +deeperfly init [OUTPUT] [--force] +``` + +Writes the packaged, fully commented default config so you can edit it in place. + +| Argument / option | Default | Meaning | +| --- | --- | --- | +| `OUTPUT` | `config.toml` | Destination path. | +| `--force` | off | Overwrite an existing file (otherwise it errors). | + +```bash +deeperfly init # -> config.toml +deeperfly init rig.toml --force # overwrite rig.toml +``` + +See [Writing configs](configuration.md) for what to edit. + +## `deeperfly run` — the pipeline + +```bash +deeperfly run INPUT... [-r] [-c CONFIG] [-o OUTPUT_DIR] [--overwrite [STAGE...]] +``` + +Detects 2D pose → bundle-adjusts the cameras → triangulates to 3D → renders the +videos, running only the [enabled stages](configuration.md#choose-which-stages-run-pipeline) +and reusing any cached results whose config is unchanged. + +| Argument / option | Default | Meaning | +| --- | --- | --- | +| `INPUT...` | — | One or more recording directories and/or wildcard patterns. | +| `-r`, `--recursive` | off | Treat each `INPUT` as a parent directory and run every recording nested under it. | +| `-c`, `--config` | snapshot, else packaged default | The merged config TOML (from `deeperfly init`). | +| `-o`, `--output-dir` | `/deeperfly_outputs/` | Where outputs go (created if missing). | +| `--overwrite [STAGE...]` | off | Force a recompute even when nothing changed (see [below](#resuming-and-recomputing)). | + +### Inputs: single, batch, recursive + +`INPUT` is a recording directory holding the configured per-camera footage, or a +wildcard matching several. Several inputs or a wildcard run as a **batch**; +recordings that don't match the config are skipped. + +```bash +deeperfly run recording/ # one recording +deeperfly run 'fly*' # batch: fly1/, fly2/, ... (quote the glob) +deeperfly run -r experiments/ # every recording nested under experiments/ +``` + +In a batch each recording runs independently: a failure is logged and the batch +continues, then `run` exits non-zero listing the failures. A single recording +fails fast. + +### Output directory + +By default each recording's outputs go to `/deeperfly_outputs/`. With +`-o`: + +- a path ending in `/` collects one subdirectory per recording under it (on a + name collision it falls back to mirroring the input tree, after confirming); +- a relative name without `/` creates that directory inside each recording; +- for a single recording, `-o` is just that recording's output directory. + +Each output directory holds `poses.h5`, the rendered MP4s, a `config.toml` +snapshot, and `run.json` — see the +[output-format reference](../reference/output-format.md). + +### Which config is used + +`-c` wins when given (and refreshes the snapshot). Without `-c`, a run reuses the +`config.toml` already in the output directory; with neither, the packaged default +is used. So two workflows both work: keep your own config and pass `-c` each +time, or edit the snapshot in the output dir and re-run with just `-o`. + +### Resuming and recomputing + +An enabled stage **reuses its cached result while its config is unchanged and its +output is present** — so re-running a finished recording is a cheap no-op, and +editing the config recomputes exactly the affected stages (and the ones after +them). Tweak `[triangulation]` or the videos and re-run: the slow 2D detection is +reused, only triangulation/visualization recompute. Each stage records its +parameters in `run.json` when it completes; performance-only knobs (`batch_size`, +`decode_buffer`, `[io.image]`) never trigger a recompute. + +`--overwrite` forces a recompute even when nothing changed — bare redoes every +stage, or name stages to redo only those (plus the stages after them): + +```bash +deeperfly run recording/ --overwrite # recompute everything +deeperfly run recording/ --overwrite pose2d visualization # just these (+ what follows) +``` + +The cached 2D pose always feeds the stages downstream, so `do_pose2d = false` +reconstructs 3D from a stored 2D pose without re-detecting. A *derived* stage's +cached output (bundle adjustment, pictorial structures, triangulation) feeds +downstream only while that stage is enabled. An enabled stage whose input is +unavailable is skipped, with the reason logged. The caching model is explained in +the [pipeline explainer](../explanation/pipeline.md#caching-and-re-runs). + +## `deeperfly inspect` — summarize a result + +```bash +deeperfly inspect RESULT.h5 +``` + +Prints the file path, the views (count and camera names), the frame count, the +skeleton (name and point count), whether a 3D pose is present, and the median / +max reprojection error. + +```bash +deeperfly inspect recording/deeperfly_outputs/poses.h5 +``` + +## `deeperfly doctor` — report the install + +```bash +deeperfly doctor +``` + +Reports, each guarded so a missing piece is shown rather than crashing: + +- **deeperfly** — version and install location. +- **system** — Python version/implementation and platform. +- **inference** — PyTorch version, CUDA/MPS availability, GPU memory. +- **frame I/O** — PyAV for video, OpenCV for images. +- **weights** — the detector-weight cache directory and whether they're + downloaded. +- **config** — the packaged default config path. + +Run it right after installing to confirm the GPU and frame-I/O backends are +available. diff --git a/docs/configuration.md b/docs/guides/configuration.md similarity index 62% rename from docs/configuration.md rename to docs/guides/configuration.md index 4edd525..0e896a0 100644 --- a/docs/configuration.md +++ b/docs/guides/configuration.md @@ -1,4 +1,4 @@ -# Configuring a run +# Writing configs A run is driven by a single self-contained `config.toml`. `deeperfly init config.toml` writes a fully commented copy to edit in place; `deeperfly run @@ -6,19 +6,22 @@ recording/` with no `-c` falls back to the packaged defaults. A single file carries everything a run needs — the camera rig, which file belongs to which camera, the detector, the pipeline and the visualization. -The sections below are ordered roughly by how often you'll touch them: the first -few you'll set for almost every recording, the last few you can usually leave at -their defaults. +This guide walks through customizing a config, ordered roughly by how often +you'll touch each section: the first few you'll set for almost every recording, +the last few you can usually leave at their defaults. For an exhaustive +parameter-by-parameter listing (every key, its type and default), see the +[configuration reference](../reference/configuration.md). ## The detection plan 2D detection is described by the top-level `[[sources]]` footage list plus the detector's own machinery under `[pose2d]` — `[[pose2d.preprocessors]]`, -`[[pose2d.models]]`, `[[pose2d.pathways]]` — and a `[pose2d.output_points]` mapping -table. A neural network turns a preprocessed image into output channels; the plan -says which footage feeds which model (the pathways) and where each output channel -lands in the skeleton (`[pose2d.output_points]`). The default fly rig is **7 -sources → 8 pathways → 7 views** (the front camera is read twice, once mirrored). +`[[pose2d.models]]`, `[[pose2d.pathways]]` — and a `[pose2d.output_points]` +mapping table. A neural network turns a preprocessed image into output channels; +the plan says which footage feeds which model (the pathways) and where each +output channel lands in the skeleton (`[pose2d.output_points]`). The default fly +rig is **7 sources → 8 pathways → 7 views** (the front camera is read twice, once +mirrored). **Sources** name the footage, the one setting almost every recording needs. Each `filename` is a glob matched inside the recording directory: @@ -65,37 +68,37 @@ mean = 0.22 n_out_channels = 19 ``` -**Pathways** are named `source -> preprocessor -> model` inference runs. A pathway -only says *what to detect on*; each needs a unique `name`: +**Pathways** are named `source -> preprocessor -> model` inference runs. A +pathway only says *what to detect on*; each needs a unique `name`: ```toml [[pose2d.pathways]] -name = "rh_noflip"; source = "vid_rh"; preprocessor = "noflip"; model = "deepfly2d" -[[pose2d.pathways]] # the front source, mirrored pass -name = "f_fliplr"; source = "vid_f"; preprocessor = "fliplr"; model = "deepfly2d" +name = "rh"; source = "vid_rh"; model = "deepfly2d" # no preprocessor = identity +[[pose2d.pathways]] # the front source, mirrored pass +name = "f_flip"; source = "vid_f"; preprocessor = "flip"; model = "deepfly2d" ``` -**`[pose2d.output_points.]`** says *where the outputs land*: for each view, a table -keyed by point name where `point = { pathway, out_channel }` fills that point from -output channel `out_channel` of the named pathway. Keying on `(view, point)` makes -every point's data come from exactly one place (a duplicate is a config error); a -`(view, point)` no entry names is left unobserved (NaN) — that union *is* the -visibility, with no separate table. +**`[pose2d.output_points.]`** says *where the outputs land*: for each view, +a table keyed by point name where `point = { pathway, out_channel }` fills that +point from output channel `out_channel` of the named pathway. Keying on `(view, +point)` makes every point's data come from exactly one place (a duplicate is a +config error); a `(view, point)` no entry names is left unobserved (NaN) — that +union *is* the visibility, with no separate table. ```toml [pose2d.output_points.rh] # right-side view: 19 channels of one pathway -rf_thorax_coxa = { pathway = "rh_noflip", out_channel = 0 } +rf_thorax_coxa = { pathway = "rh", out_channel = 0 } # ... through ... -r_abdomen2 = { pathway = "rh_noflip", out_channel = 18 } +r_abdomen2 = { pathway = "rh", out_channel = 18 } [pose2d.output_points.f] # one view fed by two pathways, disjoint points -rf_femur_tibia = { pathway = "f_noflip", out_channel = 2 } # right, un-flipped -lf_femur_tibia = { pathway = "f_fliplr", out_channel = 2 } # left, mirrored +rf_femur_tibia = { pathway = "f", out_channel = 2 } # right, un-flipped +lf_femur_tibia = { pathway = "f_flip", out_channel = 2 } # left, mirrored ``` This modularity supports a range of setups: a single front model predicting both -legs, per-view or per-side specialized models (… → 14 pathways → 7 views), or a -different `model` per pathway. +legs, per-view or per-side specialized models, or a different `model` per +pathway. ## Choose which stages run — `[pipeline]` @@ -113,36 +116,10 @@ do_visualization = true # render the videos Each enabled stage has its own top-level `[]` parameter table (below). Pictorial structures is the opt-in stage most commonly flipped on. -## Resume and recompute — fingerprints and `--overwrite` - -An *enabled* stage reuses its result while its config is unchanged and its -output is in the output directory — so re-running a finished recording is a -cheap no-op, and **editing the config recomputes exactly the affected stages**. -Tweak `[triangulation]` or the videos and re-run: the slow 2D -detection is reused, only triangulation/visualization recompute (each stage's -parameters are recorded in `/run.json` when it completes). -Performance-only knobs (`batch_size`, `decode_buffer`, `[io.image]`) never -trigger a recompute; a change that invalidates the slow `pose2d` stage is -announced loudly with exactly what changed. `--overwrite` forces a recompute -even when nothing changed: bare redoes every stage, or name stages to redo only -those (plus the stages after them): - -```bash -deeperfly run recording/ --overwrite # recompute everything -deeperfly run recording/ --overwrite pose2d visualization # just these (+ what follows) -``` - -The `pose2d` cache always feeds the stages downstream — `do_pose2d = false` -reconstructs 3D from a cached 2D pose without re-running detection. A *derived* -stage's cached output (bundle adjustment, pictorial structures, triangulation) -feeds downstream only while that stage is enabled: turning -`do_pictorial_structures` off re-triangulates from the raw detections. An -enabled stage whose input is unavailable is skipped, with the reason logged. - -The run's config is snapshotted to `/config.toml`. On a re-run `-c` -wins when given (and refreshes the snapshot); without `-c` the snapshot is -reused — so both workflows work: edit `out/config.toml` and re-run with -`-o out/` alone, or keep your own config and pass `-c` each time. +Editing the config and re-running recomputes exactly the stages you changed (and +the ones after them); the slow `pose2d` cache is reused untouched. That +resume/recompute behavior — and `--overwrite` — is covered in the +[CLI guide](cli.md#resuming-and-recomputing). ## Tune the opt-in stage — pictorial structures @@ -155,17 +132,16 @@ temporal = false # add a temporal-consistency term lam = 1.0 # bone-length prior weight ``` -Candidate peaks are extracted during detection and cached in `poses.h5` when -this stage is enabled. Enabling it on an existing output directory therefore -re-runs `pose2d` once (announced loudly); after that, tweaking `temporal` / -`lam` re-runs only the recovery from the cached candidates. Resuming with -`do_pose2d = false` from a 2D result that stored no candidates skips the stage -with a notice. +Candidate peaks are extracted during detection and cached in `poses.h5` when this +stage is enabled. Enabling it on an existing output directory therefore re-runs +`pose2d` once (announced loudly); after that, tweaking `temporal` / `lam` re-runs +only the recovery from the cached candidates. Resuming with `do_pose2d = false` +from a 2D result that stored no candidates skips the stage with a notice. ## Output videos — `[visualization]` -Each `[[visualization.videos]]` is one output MP4, composited from an -ordered list of `panels`; each panel draws one op (`imshow`, `skeleton_2d`, +Each `[[visualization.videos]]` is one output MP4, composited from an ordered +list of `panels`; each panel draws one op (`imshow`, `skeleton_2d`, `skeleton_3d`) for one camera view at a pixel offset. Common edits: ```toml @@ -181,9 +157,11 @@ skeleton_3d = { line_thickness = 2, width = 480, height = 240 } ``` The generated config ships two montage videos (`pose2d`, `pose3d`) wired to the -7-camera rig; reorder, drop, or add `panels` to change the layout. Draw-op -kwargs merge across three levels (global → per-video → per-panel), most specific -winning. Video frames are read and written with PyAV. +7-camera rig; reorder, drop, or add `panels` to change the layout. Draw-op kwargs +merge across three levels (global → per-video → per-panel), most specific +winning. Video frames are read and written with PyAV. See the +[configuration reference](../reference/configuration.md#visualization) for the +full panel and kwargs schema. ## Triangulation — `[triangulation]` @@ -200,7 +178,9 @@ weigh_by_confidence = false # weight the DLT by detector confidence ``` `ransac` keeps the largest multi-view consensus; `greedy` drops the -worst-reprojecting view; `dlt` is plain least-squares with no outlier handling. +worst-reprojecting view; `dlt` is plain least-squares with no outlier handling +(the [pipeline explainer](../explanation/pipeline.md#3d-reconstruction-triangulation-pictorial) +compares them). `weigh_by_confidence` scales each view's contribution to the DLT by `sqrt(confidence)`, so surer detections pull the 3D point harder (non-positive or @@ -224,22 +204,22 @@ These are the `[pose2d]` table's performance knobs; *what* to detect (sources, models, pathways — including per-model `weights`) is the detection plan, which shares the same `[pose2d]` table (and the top-level `[[sources]]`) and is documented above. `batch_size` is the GPU forward batch; `decode_buffer` is a -*memory* knob (peak -frames per camera is `~(decode_buffer + 2) * batch_size`) — raise it to keep the -GPU fed when decode is jittery, lower it to shave memory. +*memory* knob (peak frames per camera is `~(decode_buffer + 2) * batch_size`) — +raise it to keep the GPU fed when decode is jittery, lower it to shave memory. +These knobs never invalidate a cache. ## Frame I/O — `[io]` -Video files are read and written with PyAV (in-process FFmpeg, on the CPU); -image sequences are decoded with OpenCV. The only knob is the image-decode -thread count: +Video files are read and written with PyAV (in-process FFmpeg, on the CPU); image +sequences are decoded with OpenCV. The only knob is the image-decode thread +count: ```toml [io.image] -# workers = 0 # decode threads (0 = one per CPU) +workers = 0 # decode threads (0 = one per CPU) ``` -See [video.md](video.md) for the reader API. +The reader/writer API is in the [library guide](library.md#frame-io). ## Preprocessor op grammar — `[[pose2d.preprocessors]]` `ops` @@ -259,9 +239,9 @@ ops = [ ] # interpolation = "bilinear"|"nearest" ``` -A pathway's detections are mapped back into its view frame by inverting these -ops (plus the model's resize to its `input_size`), so the points always land in -the raw source frame the view's intrinsics describe. The flip is therefore a +A pathway's detections are mapped back into its view frame by inverting these ops +(plus the model's resize to its `input_size`), so the points always land in the +raw source frame the view's intrinsics describe. The flip is therefore a detector-input concern only — it never reflects the reconstructed 3D skeleton. ## Bundle adjustment — `[bundle_adjustment]` @@ -276,24 +256,24 @@ change them. points_to_use = [ "..." ] # skeleton point names that drive bundle adjustment (default: the 30 leg points) fixed = ["*.intr", "f.rvec", "f.tvec", "rm.tvec[2]"] # held constant; fixes the world gauge shared = [] # e.g. [["lf.tvec[2]", "rf.tvec[2]"]] to tie cameras' z distances -weigh_by_confidence = true # scale each reprojection residual by sqrt(confidence) -max_nfev = 2000 # forwarded to scipy.optimize.least_squares +weigh_by_confidence = false # scale each reprojection residual by sqrt(confidence) +max_frames = 200 # bundle-adjust on at most this many frames (subsampled) +frame_sampling = "even" # even | confidence | coverage | diversity +max_nfev = 2000 # forwarded to scipy.optimize.least_squares loss = "linear" ``` -`weigh_by_confidence` (default `true`) makes surer detections pull the -bundle adjustment harder, scaling each reprojection residual by `sqrt(confidence)`; -non-positive or non-finite confidences drop the observation, and if *every* -weight is zero it falls back to uniform weighting. Set it `false` to weight all -observations equally. (This is the mirror of -`[triangulation].weigh_by_confidence`, which defaults `false`.) - -See [library.md](library.md) for calling the bundle adjuster directly. +The `fixed` / `shared` grammar (`"*.intr"`, `"f.rvec"`, `"rm.tvec[2]"`, tying +`[["lf.tvec[2]", "rf.tvec[2]"]]`) anchors the world gauge and ties parameters +between cameras; the [reference](../reference/configuration.md#bundle_adjustment) +gives the full grammar and the `frame_sampling` strategies. See the +[library guide](library.md#geometry-and-bundle-adjustment) for calling the bundle +adjuster directly. ## Camera rig geometry — `[cameras.defaults]` and `[cameras.*]` A `[cameras.]` is a geometric **view** that a pathway maps its points back -into — pure geometry now (intrinsics + orbit extrinsics), no footage or +into — pure geometry (intrinsics + orbit extrinsics), no footage or preprocessing. The cameras orbit an object near the world origin; `[cameras.defaults]` is merged into every view, and each `[cameras.]` overrides it (the default rig sets just `azimuth_deg` per view). A view's @@ -311,10 +291,16 @@ elevation_deg = 0.0 azimuth_deg = 0 ``` +The orbit parameters (`look_at`, `distance`, `azimuth_deg`, `elevation_deg`, +`roll_deg`) and intrinsics are detailed in the +[reference](../reference/configuration.md#cameras). + ## Skeleton — `[skeleton]` The tracked points and their structure (38-point, 7-camera *Drosophila* rig): `point_names`, `limb_points` kinematic chains (each a list of point names), and the plotting `limb_palette`. Which view sees which point is not set here — it is -the union of the `[pose2d.output_points]` tables. Edit this only to track a different -animal — see [library.md](library.md) and [architecture.md](architecture.md). +the union of the `[pose2d.output_points]` tables. Edit this only to track a +different animal — see the +[reference](../reference/configuration.md#skeleton) and the +[pipeline explainer](../explanation/pipeline.md). diff --git a/docs/guides/library.md b/docs/guides/library.md new file mode 100644 index 0000000..d1ce2d8 --- /dev/null +++ b/docs/guides/library.md @@ -0,0 +1,132 @@ +# Using the library API + +deeperfly is a Python library as well as a CLI. The public API lives at the top +level (`from deeperfly import ...`): `Config`, `Camera`, `CameraGroup`, +`Skeleton`, `PoseResult`, `Recording`, `bundle_adjust`, +`bundle_adjust_from_config`, `run_from_points2d`, `run_recording`, +`resolve_recordings`, `detect_2d`, `load_detector`, and the `geometry`, +`triangulation`, `pictorial`, `pipeline`, `recordings`, and `io` submodules. This +guide shows the common tasks; the [API reference](../reference/api.md) documents +every symbol. + +Add it to your project — prefix with `UV_TORCH_BACKEND=auto` so uv picks the +right PyTorch wheel (`uv add` has no `--torch-backend` flag): + +```bash +UV_TORCH_BACKEND=auto uv add git+https://github.com/NeLy-EPFL/deeperfly +``` + +Sections of a `config.toml` are independently usable: load it once with +`Config.from_toml`, then `CameraGroup.from_config(config)` reads only the +cameras, `Skeleton.from_config(config)` only `[skeleton]`. Foreign sections are +ignored, so a config you only use for its rig needs no detector or visualization +keys. + +## Geometry and bundle adjustment + +```python +from deeperfly import CameraGroup, Config, bundle_adjust + +group = CameraGroup.from_config(Config.from_toml("config.toml")) +pts2d = group.project(pts3d) # (V, N, 2) observations +result, optimized, points = bundle_adjust(group, pts2d, fixed=["*.intr"]) +``` + +`bundle_adjust` returns the raw `scipy.optimize.OptimizeResult`, the refined +`CameraGroup`, and the refined 3D points. `fixed` / `shared` anchor the world +gauge with the same grammar as the config (`"*.intr"`, `"f.rvec"`, tying +`[["lf.tvec[2]", "rf.tvec[2]"]]`); extra keyword arguments +(`max_nfev`, `loss`, `f_scale`, ...) pass straight to scipy. +`bundle_adjust_from_config(config, pts2d)` drives the same call from a config's +`[bundle_adjustment]` section. + +The lower-level `deeperfly.geometry` module holds the JAX projection / +triangulation / Rodrigues primitives (JIT- and grad-friendly, float64 on the +CPU), and `deeperfly.triangulation` the NaN-aware DLT and RANSAC helpers. + +## The full 2D→3D pipeline from a 2D detection array + +If you already have 2D detections, `run_from_points2d` runs the array pipeline +(no files): optional bundle adjustment → 3D reconstruction, returning a +`PoseResult`. + +```python +from deeperfly import CameraGroup, Config, Skeleton, run_from_points2d + +cameras = CameraGroup.from_config(Config.from_toml("config.toml")) +result = run_from_points2d(cameras, Skeleton.fly(), pts2d, conf) +result.save("fly.h5") +``` + +`run_from_points2d(..., triangulation=..., do_pictorial=...)` selects the 3D +reconstruction: `triangulation` is `ransac` (default), `greedy` or `dlt`, and +`do_pictorial=True` runs pictorial-structures peak recovery first (see the +[pipeline explainer](../explanation/pipeline.md)). Unobserved points are expected +to already be NaN. + +## Running a recording with caching + +`run_recording` is the staged run behind `deeperfly run`: it resolves the config +against an output directory, runs the enabled `[pipeline]` stages, and reuses +cached results whose config is unchanged. + +```python +from deeperfly import resolve_recordings, run_recording +from deeperfly import Config + +config = Config.default() +for src, sources in resolve_recordings(["recordings/fly1"], config=config): + run_recording(None, src / "deeperfly_outputs", sources=sources) +``` + +To run detection yourself, `load_detector` loads the PyTorch model and +`detect_2d` streams 2D detection over a recording given a detection plan. + +## Inspecting a result + +`PoseResult` is the assembled, self-contained result — the cameras, skeleton, +`pts2d` `(V, T, P, 2)`, `conf`, `pts3d` `(T, P, 3)` and `reproj_error`. It +round-trips through HDF5 and reconstructs the cameras and skeleton, so a result +is portable without the original config. + +```python +from deeperfly import PoseResult + +result = PoseResult.load("recording/deeperfly_outputs/poses.h5") +print(result.n_views, result.n_frames) +xyz = result.pts3d # (T, P, 3), NaN where un-triangulated +``` + +See the [output-format reference](../reference/output-format.md) for the on-disk +schema and the "best available" assembly rule `load` applies. + +## Frame I/O + +`deeperfly.io` reads and writes frames through **PyAV** (in-process FFmpeg, with +libx264 bundled in the wheel — no system FFmpeg needed). All decoding and +encoding runs on the CPU and yields `(T, H, W, 3)` uint8 RGB NumPy. + +`open_reader(source)` resolves a source to a `VideoReader` (a video file) or an +`ImageSequenceReader` (a directory, glob, or explicit file list), both subclasses +of `FrameReader`. You then index it, stream it, or probe `count` / `fps`. Image +sequences are decoded by OpenCV, in parallel across threads. + +```python +from deeperfly import io + +reader = io.open_reader(path) # video file or image dir/glob/list +frames = reader[:] # (T, H, W, 3) uint8 NumPy (host) +clip = io.VideoReader("clip.mp4")[[0, 50]] # random access (seeks per frame) +for block in io.open_reader(path).stream_blocks(block_size=64): # forward, low memory + ... + +# VideoWriter encodes a frame, a batch, or any iterable -- so a long clip can be +# written as it is produced, without ever holding every frame in memory. +with io.VideoWriter("out.mp4", fps=30) as writer: + writer.write_frames(frames) # or write_frame() per frame +``` + +## Examples + +- [`examples/bundle_adjustment.ipynb`](https://github.com/NeLy-EPFL/deeperfly/blob/main/examples/bundle_adjustment.ipynb) — the bundle-adjustment walkthrough. +- [`examples/pipeline_walkthrough.ipynb`](https://github.com/NeLy-EPFL/deeperfly/blob/main/examples/pipeline_walkthrough.ipynb) — the pipeline one stage at a time. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..949b8d0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,53 @@ +# deeperfly + +Markerless 3D pose estimation of tethered *Drosophila* from a multi-camera rig. +`deeperfly` estimates camera parameters and 2D/3D keypoint locations from +behavioral recordings through one linear pipeline: **2D pose → bundle adjustment +→ triangulation → visualization**. + +It is both a command-line tool and a Python library, and a modern rewrite of +[DeepFly3D](https://github.com/NeLy-EPFL/DeepFly3D), +[DeepFly2D](https://github.com/NeLy-EPFL/DeepFly2D) and +[PyBundleAdjustment](https://github.com/semihgunel/PyBundleAdjustment). + +## Install + +```bash +uv tool install git+https://github.com/NeLy-EPFL/deeperfly --python 3.13 --torch-backend=auto +``` + +## Run + +```bash +deeperfly doctor # check the install +deeperfly init config.toml # write a config to edit +deeperfly run recording/ -c config.toml # 2D -> 3D -> video +deeperfly inspect recording/deeperfly_outputs/poses.h5 # summarize the result +``` + +The config is optional — `deeperfly run recording/` uses sensible defaults. + +## Where to go next + +
+ +- :material-rocket-launch: **[Getting started](getting-started.md)** — run the + bundled example end to end, from install to a rendered 3D video. +- :material-console: **[CLI usage](guides/cli.md)** — every command and flag. +- :material-file-cog: **[Writing configs](guides/configuration.md)** — the + `config.toml`, section by section. +- :material-language-python: **[Library API](guides/library.md)** — use the + pipeline, bundle adjustment and geometry from Python. +- :material-sitemap: **[How it works](explanation/pipeline.md)** — the pipeline + stage by stage, plus the [conventions](explanation/conventions.md) the whole + package shares. +- :material-book-open-variant: **Reference** — the complete + [configuration](reference/configuration.md), + [output format](reference/output-format.md) and + [library API](reference/api.md). + +
+ +## License + +GPL-3.0-only. See [LICENSE](https://github.com/NeLy-EPFL/deeperfly/blob/main/LICENSE). diff --git a/docs/library.md b/docs/library.md deleted file mode 100644 index 939b973..0000000 --- a/docs/library.md +++ /dev/null @@ -1,57 +0,0 @@ -# Library usage - -The public API lives at the top level (`from deeperfly import ...`): `Config`, -`Camera`, `CameraGroup`, `Skeleton`, `PoseResult`, `bundle_adjust`, -`bundle_adjust_from_config`, `run_from_points2d`, `run_recording`, and the -`geometry`, `triangulation`, `pictorial`, `pipeline`, `recordings`, and `io` -submodules. - -Sections of a `config.toml` are independently usable: load it once with -`Config.from_toml`, then `CameraGroup.from_config(config)` reads only the -cameras, `Skeleton.from_config(config)` only `[skeleton]`. - -## Geometry / bundle adjustment only - -```python -from deeperfly import CameraGroup, Config, bundle_adjust - -group = CameraGroup.from_config(Config.from_toml("config.toml")) -pts2d = group.project(pts3d) # (V, N, 2) observations -result, optimized, points = bundle_adjust(group, pts2d, fixed=["*.intr"]) -``` - -## The full 2D→3D pipeline from an existing 2D detection array - -```python -from deeperfly import CameraGroup, Config, Skeleton, run_from_points2d - -cameras = CameraGroup.from_config(Config.from_toml("config.toml")) -result = run_from_points2d(cameras, Skeleton.fly(), pts2d, conf) -result.save("fly.h5") -``` - -`run_from_points2d(..., triangulation=..., do_pictorial=...)` selects the 3D -reconstruction: `triangulation` is `ransac` (default), `greedy` or `dlt`, and -`do_pictorial=True` runs pictorial-structures peak recovery first (see -[architecture.md](architecture.md)). - -## Frame I/O - -`deeperfly.io` reads and writes frames through `pyav` (in-process FFmpeg, CPU); -`open_reader(source)` returns a `VideoReader` or `ImageSequenceReader` you then -index (`reader[:]`, `reader[i]`, `reader[[0, 3, 5]]`) or stream -(`stream_frames` / `stream_blocks`). See [video.md](video.md) for details. - -```python -from deeperfly import io - -frames = io.open_reader(path)[:] # video file or image dir -frames = io.VideoReader("clip.mp4")[[0, 50]] # random access -with io.VideoWriter("out.mp4", fps=30) as writer: - writer.write_frames(frames) # or write_frame() per frame -``` - -## Examples - -- [`examples/bundle_adjustment.ipynb`](../examples/bundle_adjustment.ipynb) — the bundle-adjustment walkthrough. -- [`examples/pipeline_walkthrough.ipynb`](../examples/pipeline_walkthrough.ipynb) — the pipeline one stage at a time. diff --git a/docs/reference/api.md b/docs/reference/api.md new file mode 100644 index 0000000..6bb1db3 --- /dev/null +++ b/docs/reference/api.md @@ -0,0 +1,65 @@ +# Library API reference + +The complete public API, generated from the source docstrings. Everything here is +importable from the top level (`from deeperfly import ...`). For task-oriented +examples see the [library guide](../guides/library.md); for the array and +coordinate conventions these functions share, see +[Conventions & glossary](../explanation/conventions.md). + +## Configuration + +::: deeperfly.config.Config + +## Cameras + +::: deeperfly.cameras.Camera + +::: deeperfly.cameras.CameraGroup + +## Skeleton + +::: deeperfly.skeleton.Skeleton + +## Results + +::: deeperfly.results.PoseResult + +## Recordings + +::: deeperfly.recordings.Recording + +::: deeperfly.recordings.resolve_recordings + +## Bundle adjustment + +::: deeperfly.bundle_adjustment.bundle_adjust + +::: deeperfly.bundle_adjustment.bundle_adjust_from_config + +## Pipeline + +::: deeperfly.pipeline.run_from_points2d + +::: deeperfly.pipeline.run_recording + +## 2D detection + +::: deeperfly.pose2d.detector.load_detector + +::: deeperfly.pose2d.stream.detect_2d + +## Geometry primitives + +::: deeperfly.geometry + +## Triangulation helpers + +::: deeperfly.triangulation + +## Pictorial structures + +::: deeperfly.pictorial + +## Frame I/O + +::: deeperfly.io diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 0000000..f72a8d8 --- /dev/null +++ b/docs/reference/configuration.md @@ -0,0 +1,305 @@ +# Configuration reference + +Every key of the `config.toml`, by section. For a task-oriented walkthrough of +how to customize a config, start with [Writing configs](../guides/configuration.md); +this page is the exhaustive listing. + +A config is one TOML file. Each stage reads its parameters through a typed +accessor whose **defaults are the single source of truth** (the frozen `*Params` +dataclasses in `src/deeperfly/config.py`); the packaged +`default_config.toml` mirrors them exactly. An unknown key in a stage table is a +hard error that names the allowed keys. Performance-only knobs (`batch_size`, +`decode_buffer`, `[io.image]`) never invalidate a stage's cache; everything else +that affects a result does. + +The top-level layout: + +```toml +[[sources]] # footage globs (shared input) +[io.image] # image-sequence decode +[skeleton] # tracked points and limbs +[cameras.defaults] # rig geometry: shared defaults +[cameras.] # rig geometry: per-view overrides +[pipeline] # which stages run +[pose2d] # 2D detection: knobs + detection plan sub-tables +[bundle_adjustment] # camera refinement +[pictorial_structures] # opt-in peak recovery +[triangulation] # 2D -> 3D +[visualization] # output videos +``` + +## `[[sources]]` — footage { #sources } + +An array of tables; each names a footage glob matched inside the recording +directory. A source can feed several pathways and a visualization `imshow` panel. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | str | *required* | Source identifier (referenced by pathways and views). | +| `filename` | str | the `name` | Glob inside the recording dir: a named file (`camera_0.mp4`), a bare prefix (`camera_1` → `camera_1*`, a video or image sequence), or a wildcard. | + +A source's footage is one video file or a naturally-sorted image sequence. A +directory is a valid recording only when every source matches footage with the +same file/frame count. + +## `[io.image]` — image decode { #io } + +Video files use PyAV; image sequences use OpenCV. The only knob: + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `workers` | int | `0` | Image-decode threads. `0` = auto (one per CPU). | + +## `[skeleton]` — tracked points { #skeleton } + +The tracked points and their structure. Omit the section entirely to use the +default 38-point fly skeleton. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | str | `"skeleton"` | Skeleton identifier (e.g. `"fly38"`). | +| `point_names` | list[str] | *required* | Ordered tracked-point names; the length is `P`. | +| `limb_points` | table | `{}` | `[skeleton.limb_points]`: each limb name → its points in kinematic-chain order. | +| `limb_palette` | table | `{}` | `[skeleton.limb_palette]`: each limb name → a hex plotting color. Limbs without an entry fall back to a default colormap. | + +```toml +[skeleton] +name = "fly38" +point_names = ["lf_thorax_coxa", "lf_coxa_trochanter", "..."] + +[skeleton.limb_points] +lf_leg = ["lf_thorax_coxa", "lf_coxa_trochanter", "lf_femur_tibia", "lf_tibia_tarsus", "lf_claw"] + +[skeleton.limb_palette] +lf_leg = "#0f7399" +``` + +Which view sees which point is **not** set here — it is the union of the +[`[pose2d.output_points]`](#output_points) tables. + +## `[cameras.*]` — rig geometry { #cameras } + +Each `[cameras.]` is a geometric view: pure intrinsics + extrinsics, no +footage. `[cameras.defaults]` is merged into every view; per-view tables override +it (the default rig sets just `azimuth_deg` per view). A view's intrinsics +describe the raw frame of the source feeding it. + +**Intrinsics:** + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `focal_length_px` | float or [float, float] | *required* | `[fx, fy]` in raw-frame pixels (a scalar is allowed when `fx == fy`). | +| `principal_point_px` | [float, float] | image center `((w-1)/2, (h-1)/2)` | Principal point `[cx, cy]`. Omit to use each view's image center. | +| `distortion_coefficients` | list[float] | `[]` | OpenCV-ordered distortion coefficients; empty means no distortion. | + +**Extrinsics (orbit / look-at):** the cameras orbit a target near the world +origin. World up is `+z`. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `look_at` | [float, float, float] | `[0, 0, 0]` | World point the camera looks at. | +| `distance` | float | *required* | Distance from `look_at` to the camera center. | +| `azimuth_deg` | float | `0.0` | Longitude around `look_at`. | +| `elevation_deg` | float | `0.0` | Latitude above the horizon (±90 is undefined — the roll becomes ambiguous). | +| `roll_deg` | float | `0.0` | Rotation about the optical axis. | + +Explicit `rvec` / `tvec` / `rotation_matrix` / `position` keys are **not** +accepted in the config (they are rejected with a pointer to the orbit keys); use +the orbit parameters. The internal `CameraGroup` still uses `rvec` / `tvec`. + +```toml +[cameras.defaults] +focal_length_px = [22388.125, 22388.125] +distortion_coefficients = [] +look_at = [0.0, 0.0, 0.0] +distance = 107.463 +elevation_deg = 0.0 +roll_deg = 0.0 + +[cameras.rh] +azimuth_deg = -120 +``` + +## `[pipeline]` — which stages run { #pipeline } + +One `do_` boolean per stage. Each enabled stage reads its own `[]` +table. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `do_pose2d` | bool | `true` | Detect 2D pose in every view. | +| `do_bundle_adjustment` | bool | `true` | Refine the cameras. | +| `do_pictorial_structures` | bool | `false` | DeepFly3D-style peak recovery (opt-in). | +| `do_triangulation` | bool | `true` | Triangulate 2D → 3D. | +| `do_visualization` | bool | `true` | Render the videos. | + +## `[pose2d]` — 2D detection { #pose2d } + +The `[pose2d]` table holds the detector's performance knobs *and* (as sub-tables) +the detection plan — what to detect and how. + +**Performance knobs:** + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `precision` | str | `"bfloat16"` | Forward precision: `"float32"` (reference), `"float16"` (CUDA autocast, ~1.5–2× faster), `"bfloat16"` (default, wider range). Ignored on CPU/MPS. | +| `batch_size` | int | `16` | GPU forward batch (images per forward). Clamped to ≥ 1; throughput plateaus by ~16 on a fast GPU. | +| `decode_buffer` | int | `4` | Decode queue depth, in multiples of `batch_size`. Clamped to ≥ 1. Peak frames/camera ≈ `(decode_buffer + 2) * batch_size`. | + +### `[[pose2d.preprocessors]]` + +Named, reusable frame-op pipelines, referenced by a pathway's `preprocessor`. + +| Key | Type | Description | +| --- | --- | --- | +| `name` | str | Preprocessor identifier. | +| `ops` | list[table] | Ordered frame ops (below); `[]` = identity. | + +**Ops** (run in written order; flips/rotations do not commute): + +| Op | Fields | Effect | +| --- | --- | --- | +| `fliplr` | — | Left–right flip. | +| `flipud` | — | Up–down flip. | +| `rot90` | `k` (int) | `k` counter-clockwise quarter-turns (any sign). | +| `crop` | `x`, `y`, `width`, `height` | Keep a window. | +| `resize` | `scale`, or `width`/`height`; optional `interpolation` (`"bilinear"`/`"nearest"`) | Rescale. | + +Detections are mapped back into the raw frame by inverting these ops, so a +preprocessor never moves the stored 2D or the reconstructed 3D. + +### `[[pose2d.models]]` + +A detector network and its input contract. + +| Key | Type | Description | +| --- | --- | --- | +| `name` | str | Model identifier (referenced by pathways). | +| `class` | str | Network registry key (`"hourglass"` = DeepFly2D). | +| `weights` | str | Checkpoint path; `""` / omitted uses the auto-provisioned cache. | +| `input_size` | [int, int] | `(height, width)` the network expects; frames are resized to it and peaks scaled back. | +| `mean` | float | Scalar subtracted after `/255` normalization. | +| `n_out_channels` | int | Output heatmap count (validated against the weights). | + +### `[[pose2d.pathways]]` + +A named `source → preprocessor → model` inference run. Says *what to detect on*. + +| Key | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | str | yes | Unique pathway identifier (referenced by `output_points`). | +| `source` | str | yes | The `[[sources]]` name to detect on. | +| `model` | str | yes | The `[[pose2d.models]]` name to use. | +| `preprocessor` | str | no | A `[[pose2d.preprocessors]]` name; omit for identity. | + +### `[pose2d.output_points.]` { #output_points } + +For each view, where every tracked point's data comes from. A table keyed by +point name: + +```toml +[pose2d.output_points.rh] +rf_thorax_coxa = { pathway = "rh", out_channel = 0 } +``` + +`point = { pathway, out_channel }` fills that point of the view from output +channel `out_channel` of the named pathway. Keying by `(view, point)` means each +point has exactly one source (a repeat is an error); a `(view, point)` left out +stays unobserved (`NaN`). That union is the visibility. + +## `[bundle_adjustment]` — camera refinement { #bundle_adjustment } + +Fly-as-target bundle adjustment over `scipy.optimize.least_squares`. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `points_to_use` | list[str] or omitted | the 30 leg points | Skeleton point names that drive BA. Omit the key to use all keypoints. | +| `fixed` | list[str] | `[]` | Parameters held constant (grammar below); anchors the world gauge. | +| `shared` | list[list[str]] | `[]` | Groups of parameters tied together, e.g. `[["lf.tvec[2]", "rf.tvec[2]"]]`. | +| `weigh_by_confidence` | bool | `true` | Scale each reprojection residual by `sqrt(confidence)`; zero/non-finite confidences drop the observation (all-zero falls back to uniform). | +| `max_frames` | int or omitted | `100` | Bundle-adjust on at most this many frames (subsampled). Omit / `null` for all. | +| `frame_sampling` | str | `"even"` | Which frames to keep (below). | +| *other keys* | — | — | Any remaining flat key (`max_nfev`, `loss`, `f_scale`, `tr_solver`, …) is forwarded to `scipy.optimize.least_squares`. | + +**`fixed` / `shared` grammar** — a reference is `.` with optional +indexing, and `*` wildcards the camera: + +- `"*.intr"` — every camera's intrinsics. +- `"f.rvec"`, `"f.tvec"` — the front camera's orientation / position. +- `"rm.tvec[2]"` — one component (the z distance) of a camera's translation. + +**`frame_sampling` strategies:** + +| Value | Keeps | +| --- | --- | +| `"even"` | Evenly spaced over the recording (temporal spread). | +| `"confidence"` | The highest-confidence frame in each time bin. | +| `"coverage"` | The frame in each bin with the most points seen by ≥ 2 cameras. | +| `"diversity"` | Frames whose postures are most spread apart. | + +## `[pictorial_structures]` — peak recovery { #pictorial_structures } + +Runs only when `do_pictorial_structures = true`. Operates on the detector's top-K +candidates (extracted and cached during detection). + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `k` | int | `5` | Candidate peaks per joint. | +| `temporal` | bool | `false` | Add a temporal-consistency term. | +| `lam` | float | `1.0` | Bone-length prior weight. | + +## `[triangulation]` — 2D → 3D { #triangulation } + +How the per-view 2D points become one 3D point. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `method` | str | `"ransac"` | `"ransac"` (largest multi-view consensus, robust), `"greedy"` (drop the worst-reprojecting view), or `"dlt"` (plain least-squares). | +| `ransac_threshold` | float | `15.0` | Inlier reprojection cutoff (px) for `method = "ransac"`. | +| `min_inliers` | int | `2` | Minimum agreeing views to accept a point (`ransac`). | +| `reproj_threshold` | float | `40.0` | Per-view reprojection cutoff (px) for `method = "greedy"`. | +| `max_drops` | int | `5` | Max views dropped per offending point (`greedy`). | +| `weigh_by_confidence` | bool | `false` | Scale the DLT by `sqrt(confidence)` (the mirror of the BA knob, which defaults `true`). | + +## `[visualization]` — output videos { #visualization } + +Global settings plus one `[[visualization.videos]]` per output MP4. + +**Global (`[visualization]`):** + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `background` | str or [r, g, b] | `"black"` | Canvas fill (overridable per video / per panel). | +| `output_fps` | float | input fps | Explicit output frame rate for every video. | +| `speed` | float | `1.0` | Scale the input fps instead (`0.5` = slow motion). `output_fps` wins if both are set. | + +**`[visualization.kwargs]`** — draw-op defaults shared by every video, keyed by +the `plot` op name (`imshow`, `skeleton_2d`, `skeleton_3d`). Kwargs merge across +three levels — global → per-video `kwargs` → per-panel extra keys — most specific +winning. + +**`[[visualization.videos]]`:** + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `video_name` | str | *required* | Output filename (`.mp4`). | +| `panels` | list[table] | *required* | Ordered panels (below); they draw in order, so a skeleton panel over an `imshow` at the same offset overlays it. | +| `width`, `height` | int | auto-size | Canvas size in pixels; omit to fit all panels. | +| `background` | str or [r, g, b] | inherits global | Per-video canvas fill. | +| `kwargs` | table | `{}` | Per-video draw-op kwargs (merges over the global). | + +**Panel** — one draw op for one view at a pixel offset: + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `plot` | str | *required* | `"imshow"` (the view's frame), `"skeleton_2d"` (its 2D detections), or `"skeleton_3d"` (the 3D skeleton reprojected into the view). | +| `view` | str | *required* | Camera/view name. | +| `x0`, `y0` | int | `0` | Top-left pixel of the panel. | +| `scale` | float | `1.0` | Uniform scale. | +| `width`, `height` | int | from `scale` | Target box (priority over `scale`); one given → the other follows to keep aspect. | +| `background` | str or [r, g, b] | inherits | Per-panel fill. | +| *extra keys* | — | — | Forwarded as draw-op kwargs (`point_radius`, `line_thickness`, `palette`, …). | + +A `skeleton_3d` panel needs a 3D pose; a video that requires 3D is skipped (with +a logged reason) when the result has none. Videos are encoded H.264 / libx264 via +PyAV on the CPU. diff --git a/docs/reference/output-format.md b/docs/reference/output-format.md new file mode 100644 index 0000000..6c38fcf --- /dev/null +++ b/docs/reference/output-format.md @@ -0,0 +1,106 @@ +# Output format + +A run writes everything to its output directory (`/deeperfly_outputs/` by +default, or `-o`): + +``` +deeperfly_outputs/ +├── poses.h5 # the result: cameras, skeleton, per-stage 2D/3D data +├── config.toml # byte-for-byte snapshot of the config this run used +├── run.json # per-stage fingerprints (drives cache reuse) +└── *.mp4 # one per [[visualization.videos]] entry +``` + +## `poses.h5` + +A self-contained HDF5 file (schema **version 2**). Each pipeline stage writes its +own group, so a stage never overwrites another's data and any downstream stage +can be re-run later from pristine upstream outputs. The file fully reconstructs +the cameras and skeleton, so results are portable without the original config. + +Arrays use the [view-leading layout](../explanation/conventions.md#array-layouts) +in float64; `NaN` encodes missing observations / un-triangulated points. + +```text +attrs["meta"] json: {deeperfly_format_version: 2, created_utc, ...} +skeleton/ point_names, limb_names, limb_id, bones, palette/ +pose2d/ + points (V, T, P, 2) arg-max 2D detections (visibility-masked) + conf (V, T, P) detection confidences + cameras/ the config rig as built at detect time + attrs["image_sizes"] json {camera_name: [h, w]} of the raw footage frames + candidates/ top-K peaks (xy, score) -- only if pictorial_structures + was enabled at detect time +bundle_adjustment/ + cameras/ the BA-refined rig +pictorial_structures/ + points (V, T, P, 2) PS-corrected 2D + points3d (T, P, 3) initial 3D estimate + reproj_error (V, T, P) +triangulation/ + points (V, T, P, 2) cleaned 2D (outlier-rejecting methods) + points3d (T, P, 3) + reproj_error (V, T, P) +``` + +A `cameras/` group stores `names`, `rvecs`, `tvecs`, `intrs` (`[fx, fy, cx, cy]`), +and `dists`. The `skeleton/` group stores `point_names`, `limb_names`, `limb_id`, +`bones`, and a `palette/` subgroup of limb → hex color. + +Which groups are present depends on which stages ran. A group exists only once its +stage completed; only the stages that were enabled (and whose inputs were +available) appear. + +### What the library reads back + +`PoseResult.load(path)` assembles the **most-derived data present**, so you get +the best result without knowing which stages ran: + +| Field | Preference order | +| --- | --- | +| `pts2d` | `triangulation` → `pictorial_structures` → `pose2d` | +| `pts3d` | `triangulation` → `pictorial_structures` | +| `reproj_error` | `triangulation` → `pictorial_structures` | +| `cameras` | `bundle_adjustment` → `pose2d` (config rig) | +| `conf` | `pose2d` | + +`PoseResult.save(path)` is the library one-shot (no staged groups): it writes +`pts2d`/`conf` to `pose2d/` and, when a 3D pose is present, the 2D/3D/error to +`triangulation/`, so `load` round-trips the assembled view. + +A file in an older schema version is rejected on `load` (re-run to regenerate) +and simply read as empty by the staged run (so it recomputes). + +## `config.toml` (snapshot) + +The exact config text that drove the run, copied byte-for-byte for +reproducibility. On a later run, `-c` wins when given (and refreshes this +snapshot); without `-c`, this snapshot is reused — so you can edit it in place and +re-run with just `-o`. + +## `run.json` + +A small JSON sidecar recording, per stage, the **fingerprint** (the +result-affecting config subset) and the completion time. It is outdir-local run +state, kept out of the portable `poses.h5`; deleting it merely recomputes +everything. + +```json +{ + "format_version": 1, + "stages": { + "pose2d": { "fingerprint": { "...": "..." }, "completed_utc": "2026-..." }, + "triangulation": { "fingerprint": { "...": "..." }, "completed_utc": "2026-..." } + } +} +``` + +On a re-run a stage is reused only when its recorded fingerprint still matches the +current config **and** its output is present. Comparison is *subset* semantics: a +key dropping out of the expected fingerprint (e.g. `candidates` when +`pictorial_structures` is disabled again) does not invalidate the cache, while a +changed or newly-appearing key does. Performance-only knobs (`batch_size`, +`decode_buffer`, `[io.image]`) are deliberately excluded. Fingerprints are stored +verbatim (not hashed) so a mismatch can be reported as a readable diff. See +[caching and re-runs](../explanation/pipeline.md#caching-and-re-runs) for the full +model. diff --git a/docs/video.md b/docs/video.md deleted file mode 100644 index 5e2a733..0000000 --- a/docs/video.md +++ /dev/null @@ -1,35 +0,0 @@ -# Frame I/O - -`deeperfly.io` reads and writes video files with **PyAV** — in-process libx264, -with FFmpeg bundled in the wheel (no system FFmpeg needed). All decoding and -encoding runs on the CPU and yields `(T, H, W, 3)` uint8 RGB NumPy. - -Footage is read through a small reader hierarchy: `open_reader(source)` resolves a -source to a `VideoReader` (a video file) or an `ImageSequenceReader` (a directory, -glob, or explicit file list), both subclasses of `FrameReader`. You then index -(`reader[:]`, `reader[i]`, `reader[[0, 3, 5]]`), stream (`stream_frames` / -`stream_blocks`), or probe metadata (`count` / `fps`) against the returned reader. - -Image *sequences* (a directory or glob of PNG/JPG/…) are decoded by OpenCV, in -parallel across threads (JPEG/PNG decoders release the GIL). - -```python -from deeperfly import io - -reader = io.open_reader(path) # video file or image dir/glob/list -frames = reader[:] # (T, H, W, 3) uint8 NumPy (host) -clip = io.VideoReader("clip.mp4")[[0, 50]] # random access (seeks per frame) -for block in io.open_reader(path).stream_blocks(block_size=64): # forward - ... - -# VideoWriter encodes a frame, a batch, or any iterable -- so a long clip can be -# written as it is produced, without ever holding every frame in memory. -with io.VideoWriter("out.mp4", fps=30) as writer: - writer.write_frames(frames) -``` - -`deeperfly run` decodes on the CPU and uploads each window to the detector device -in one shot — decode is not the bottleneck, the detector forward is. The only -frame-I/O configuration is the image-decode thread count (`[io.image] workers`), -applied across every stage. See the config comments and `deeperfly.io` docstrings -for details. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..fe21d23 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,89 @@ +site_name: deeperfly +site_description: Markerless 3D pose estimation of tethered Drosophila from a multi-camera rig +site_url: https://nely-epfl.github.io/deeperfly/ +repo_url: https://github.com/NeLy-EPFL/deeperfly +repo_name: NeLy-EPFL/deeperfly +edit_uri: edit/main/docs/ +copyright: GPL-3.0-only + +theme: + name: material + features: + - navigation.sections # render top-level nav groups as sidebar sections + - navigation.top # back-to-top button + - navigation.tracking # update the URL with the active anchor + - content.code.copy # copy button on code blocks + - toc.follow + - search.suggest + palette: + # light / dark toggle + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + toggle: + icon: material/weather-night + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + toggle: + icon: material/weather-sunny + name: Switch to light mode + +nav: + - Home: index.md + - Getting started: getting-started.md + - Guides: + - CLI usage: guides/cli.md + - Writing configs: guides/configuration.md + - Library API: guides/library.md + - How it works: + - Pipeline stages: explanation/pipeline.md + - Conventions & glossary: explanation/conventions.md + - Reference: + - Configuration: reference/configuration.md + - Output format: reference/output-format.md + - Library API: reference/api.md + - Contributing: https://github.com/NeLy-EPFL/deeperfly/blob/main/CONTRIBUTING.md + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.inlinehilite + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + # Render ```mermaid blocks as diagrams (the data-flow diagrams in + # "How it works" rely on this). + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + +plugins: + - search + - mkdocstrings: + handlers: + python: + # griffe reads the source statically from src/, so building the API + # reference does not import torch/jax. + paths: [src] + options: + docstring_style: numpy + docstring_section_style: table + show_root_heading: true + show_root_full_path: false + show_source: true + show_signature_annotations: true + separate_signature: true + members_order: source + filters: ["!^_"] diff --git a/pyproject.toml b/pyproject.toml index 54cf534..def1d9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ dependencies = [ [project.urls] Homepage = "https://github.com/NeLy-EPFL/deeperfly" +Documentation = "https://nely-epfl.github.io/deeperfly/" Repository = "https://github.com/NeLy-EPFL/deeperfly" Issues = "https://github.com/NeLy-EPFL/deeperfly/issues" @@ -71,6 +72,14 @@ test = [ "pytest>=9.0.3", "pytest-cov>=6.0", ] +docs = [ + # The documentation site (MkDocs + Material). The library API reference is + # generated from the source docstrings by mkdocstrings (griffe reads the + # source statically, so the heavy runtime deps are not imported to build it). + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.27", + "ruff>=0.6", # mkdocstrings uses it to format the rendered API signatures +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/uv.lock b/uv.lock index b2f5cb2..24606ea 100644 --- a/uv.lock +++ b/uv.lock @@ -50,6 +50,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/39/381104e427a0c7231d2ec0d25d538d58fc20fc0458846b95860d3ef8073b/av-17.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4", size = 21918412, upload-time = "2026-04-18T17:12:09.312Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + [[package]] name = "cffi" version = "2.0.0" @@ -98,6 +128,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -378,6 +477,11 @@ dev = [ { name = "ipykernel" }, { name = "matplotlib" }, ] +docs = [ + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "ruff" }, +] test = [ { name = "pytest" }, { name = "pytest-cov" }, @@ -405,6 +509,11 @@ dev = [ { name = "ipykernel", specifier = ">=7.2.0" }, { name = "matplotlib", specifier = ">=3.10" }, ] +docs = [ + { name = "mkdocs-material", specifier = ">=9.5" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27" }, + { name = "ruff", specifier = ">=0.6" }, +] test = [ { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=6.0" }, @@ -470,6 +579,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + [[package]] name = "h5py" version = "3.16.0" @@ -505,6 +635,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, ] +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -757,6 +896,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -892,6 +1040,134 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/b4/5fed370d8ebd96e4e399460a7146ae989263f16588b05a6facd6dbd51e60/mkdocstrings_python-2.0.4.tar.gz", hash = "sha256:58c73c5d358e64e9b1673447663f4a2f8a8941e392e225fc0a0c893758cc452f", size = 199219, upload-time = "2026-06-05T08:13:01.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/e3/00ec594aef5f55522e6d373bc2ac53e53a8f5e9ae32f2d6854b0de4270f3/mkdocstrings_python-2.0.4-py3-none-any.whl", hash = "sha256:fd87c173e1e719a85997b6d4f852cdc55f36710e0ed08da3a7bd9abe79c9db00", size = 104790, upload-time = "2026-06-05T08:13:00.393Z" }, +] + [[package]] name = "ml-dtypes" version = "0.5.4" @@ -1205,6 +1481,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "parso" version = "0.8.7" @@ -1214,6 +1499,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1376,6 +1670,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.21.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1427,6 +1734,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "pyzmq" version = "27.1.0" @@ -1475,6 +1831,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "rich" version = "15.0.0" @@ -1488,6 +1859,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + [[package]] name = "scipy" version = "1.17.1" @@ -1761,6 +2157,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "wadler-lindig" version = "0.1.7" @@ -1770,6 +2175,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/96/04e7b441807b26b794da5b11e59ed7f83b2cf8af202bd7eba8ad2fa6046e/wadler_lindig-0.1.7-py3-none-any.whl", hash = "sha256:e3ec83835570fd0a9509f969162aeb9c65618f998b1f42918cfc8d45122fe953", size = 20516, upload-time = "2025-06-18T07:00:41.684Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "wcwidth" version = "0.7.0"