diff --git a/.context/plan.md b/.context/plan.md index 2612617..aefc0d7 100644 --- a/.context/plan.md +++ b/.context/plan.md @@ -76,3 +76,61 @@ ## Notes - Correctness is defined by parity with the Fortran binary, not by convergence alone. - Detailed feature status: `feature_parity.md`; migration record: `migration_plan.md`. + +## Release Readiness (JOSS track) — started 2026-07-10 +Endgame ordering: finish the three open benchmark issues -> documentation -> +transfer to github.com/sccn -> JOSS paper. The repo will move to +`github.com/sccn/pyAMICA` (full transfer, preserving issues/PRs/history) and docs +will be hosted at `eeglab.org/pyAMICA`. + +### Phase R1: Benchmark completion (#90, #91, #92) +- [~] #90 Data-size (k-factor) frames sweep at 70ch. Code + data staged on hallu + (branch `feature/issue-90-datasize-sweep`, origin-pushed; based on current main). + Full 747,750-frame npy present (`ds002718_sub-002_eeg70_full.npy`). CUDA sweep + (torch-cuda-f64/f32) + native-fortran-f64 launched on hallu 2026-07-10: + frames 73.5k/147k/294k/490k/747.75k -> k=15/30/60/100/152, 2000 iters, + out=`benchmarks/results_k90_hallu`. Probe: largest frames ~1.1 s/it (f64) -> + ~36 min/run; CUDA sweep ETA ~2.5 h. Remaining: finish runs -> `--compare` + cross-backend |corr| vs k figure + report -> PR. +- [ ] #91 Spatially-distributed channel subsets: replace `full[:nc]` first-N slicing + in `benchmark_dimsweep.py`/`benchmark_decompose.py` with farthest-point sampling + over real electrode 3D coords (whole-head 16/32/48ch montages). Local; formalize + `mne` as a viz/benchmark extra (not in core env). Prereq for #92 reduced-montage. +- [ ] #92 EEGLAB drop-in output parity: variance-ordered ICs (back-projected variance, + IC1=highest), `loadmodout15`/`pop_runamica`-readable output, sign/scale + conventions, documented MATLAB+EEGLAB round-trip. MATLAB R2025b and EEGLAB both + present locally (`~/Documents/git/eeg/eeglab`). + +### Phase R2: Documentation (MkDocs Material, per /project:init-project) +Use the init-project docs templates verbatim where possible +(`~/.claude/plugins/cache/research-skills/project/0.5.0/templates/config/mkdocs.yml` ++ `github/workflows/docs.yml`), adapted for pyAMICA: +- Material theme (light/dark palette toggle, navigation.tabs/sections/indexes/top/ + instant, search.suggest/highlight, content.code.copy, toc.integrate). +- Plugins: `search`, `mkdocstrings` (python) with **`docstring_style: numpy`** (repo + uses numpy docstrings, NOT the template's `google` default), `git-revision-date-localized`. +- Add a `docs` optional-dependency extra (mkdocs-material, mkdocstrings[python], + mkdocs-git-revision-date-localized-plugin) — the `docs.yml` workflow runs + `uv sync --extra docs` then `uv run mkdocs build`. Currently only an `mlx` extra exists. +- **Hosting:** GitHub Pages via `docs.yml` (build+deploy on push to main). Served at + `eeglab.org/pyAMICA` because `sccn/pyAMICA` *project* Pages inherit the sccn org + Pages custom domain (`eeglab.org`) at the `/pyAMICA` subpath. `site_url: + https://eeglab.org/pyAMICA/`, relative links; stages at + `neuromechanist.github.io/pyAMICA/` pre-transfer. +- [ ] De-WIP `README.md` (drop the "do not rely on this" disclaimer; `uv` install; + quickstart; backend-selection guide MLX/CUDA/CPU + f32/f64; results table). +- [ ] mkdocs.yml + docs/ skeleton (Home, Getting Started, User Guide, API Reference + via mkdocstrings, Development, Changelog) + `docs` extra + `docs.yml` workflow. +- [ ] Community health: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `CITATION.cff`, + issue/PR templates (JOSS-expected). + +### Phase R3: Transfer to github.com/sccn (before JOSS submission) +- [ ] GitHub repo transfer (preserves issues/PRs/stars/history; auto-redirects old + URLs). Post-transfer: update badge/repo URLs (README, CITATION.cff, paper.md), + wire up the `eeglab.org/pyAMICA` docs deploy target. + +### Phase R4: JOSS paper (/manuscript:manuscript-writing) +- [ ] `paper.md` (~1000 words) + `paper.bib`: summary, statement of need (GPU + + cross-platform AMICA with Fortran parity; drop-in for EEGLAB AMICA), comparison + vs EEGLAB AMICA / Picard / FastICA, backend + parity results, acknowledgments; + `repository -> github.com/sccn/pyAMICA`. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5a5b4ac --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,48 @@ +name: Deploy Documentation + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # git-revision-date-localized needs full history, not a shallow clone + fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Set up Python + run: uv python install 3.12 + - name: Install dependencies + run: uv sync --extra docs + - name: Build documentation + run: uv run mkdocs build --strict + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site/ + + deploy-docs: + needs: build-docs + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 6acdffd..7dad816 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ bench_out/ # Decomposition benchmark run artifacts (#87) results/ + +# MkDocs build output +site/ diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..9d8f7b1 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,37 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it using the metadata below." +title: "pyAMICA: a Python implementation of Adaptive Mixture ICA" +abstract: >- + pyAMICA is a Python (PyTorch) implementation of Adaptive Mixture Independent + Component Analysis (AMICA) that reproduces the reference Fortran + implementation, with GPU (CUDA), Apple-GPU (MLX), and CPU support. It targets + EEG/EMG blind source separation. +type: software +authors: + - family-names: Shirazi + given-names: Seyed Yahya + email: shirazi@ieee.org +version: 0.1.dev0 +license: BSD-3-Clause +repository-code: "https://github.com/neuromechanist/pyAMICA" +url: "https://github.com/neuromechanist/pyAMICA" +keywords: + - AMICA + - independent component analysis + - blind source separation + - EEG + - EMG + - PyTorch +references: + - type: article + title: "AMICA: An adaptive mixture of independent component analyzers with shared components" + authors: + - family-names: Palmer + given-names: Jason A. + - family-names: Kreutz-Delgado + given-names: Kenneth + - family-names: Makeig + given-names: Scott + year: 2012 + institution: + name: Swartz Center for Computational Neuroscience, University of California San Diego diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..03a946c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +shirazi@ieee.org. All complaints will be reviewed and investigated promptly and +fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f784334 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to pyAMICA + +Thanks for your interest in contributing. pyAMICA is a Python implementation of +Adaptive Mixture Independent Component Analysis (AMICA) that reproduces the +reference Fortran implementation. Because **numerical parity with the Fortran +reference is the definition of correctness**, contributions are held to that +standard rather than to "it converges." + +## Getting help and reporting issues + +- **Questions, bugs, and feature requests:** please open an issue on the + [GitHub issue tracker](https://github.com/neuromechanist/pyAMICA/issues). +- When reporting a bug, include the pyAMICA version, platform, device + (CPU/CUDA/MPS/MLX), precision (float32/float64), and a minimal example. + +## Development setup + +pyAMICA uses [UV](https://docs.astral.sh/uv/) for environment and dependency +management. + +```bash +git clone https://github.com/neuromechanist/pyAMICA.git +cd pyAMICA +uv sync # install the project and dependencies +uv run pytest # run the test suite +``` + +On Apple MPS, run with `PYTORCH_ENABLE_MPS_FALLBACK=1` for ops MPS does not yet +support. + +## Testing + +- **Real data only.** Correctness tests use the real sample EEG and the Fortran + binary shipped in `pyAMICA/sample_data/`. Do not use mocks, stubs, or synthetic + data as the basis for a correctness test: no test is better than a fake passing + test. +- Run with coverage: `uv run pytest --cov`. +- The natural-gradient backend computes in float64 for Fortran parity; use + float64 for parity-sensitive tests. + +## Code style + +- **Lint and format** with Ruff before committing: + ```bash + uv run ruff check --fix . && uv run ruff format . + ``` +- Follow the conventions in the surrounding code and in `AGENTS.md`. +- No em-dashes in prose; define abbreviations on first use. + +## Pull requests + +1. Open an issue first (except for minor fixes). +2. Create a branch (for example `gh issue develop `). +3. Make atomic commits with concise messages (no emojis, no AI attribution). +4. Add or update tests, and run the suite before pushing. +5. Open a PR describing what changed and how it was tested. +6. Ensure CI is green before requesting a merge. + +## License + +By contributing, you agree that your contributions will be licensed under the +project's [BSD 3-Clause License](LICENSE). diff --git a/docs/api/amica.md b/docs/api/amica.md new file mode 100644 index 0000000..d1783ff --- /dev/null +++ b/docs/api/amica.md @@ -0,0 +1,6 @@ +# AMICA + +The main scikit-learn-style interface. Wraps the natural-gradient EM backend +([`AMICATorchNG`](torch-backend.md)). + +::: pyAMICA.AMICA diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..e384774 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,21 @@ +# API Reference + +The public import surface is stable: + +```python +from pyAMICA import AMICA, AMICA_NumPy, AMICATorchNG +``` + +- **[`AMICA`](amica.md)** — the main scikit-learn-style interface. Wraps the + PyTorch natural-gradient EM backend. Start here. +- **[`AMICATorchNG`](torch-backend.md)** — the PyTorch natural-gradient EM + backend (Fortran parity). The `AMICA` interface delegates to this class. +- **[`AMICA_NumPy`](numpy-backend.md)** — the legacy NumPy reference + implementation, retained as an oracle and for its command-line interface. + +The optional Apple-Silicon GPU backend is imported separately and is not part of +the default import surface: + +```python +from pyAMICA.mlx_impl import AMICAMLXNG # requires the `mlx` extra +``` diff --git a/docs/api/numpy-backend.md b/docs/api/numpy-backend.md new file mode 100644 index 0000000..389e505 --- /dev/null +++ b/docs/api/numpy-backend.md @@ -0,0 +1,7 @@ +# NumPy backend (AMICA_NumPy) + +The legacy NumPy reference implementation, retained as an oracle and for its +command-line interface. It carries the same parity fixes as the PyTorch backend, +plus baralpha and outlier rejection. + +::: pyAMICA.AMICA_NumPy diff --git a/docs/api/torch-backend.md b/docs/api/torch-backend.md new file mode 100644 index 0000000..b6a0706 --- /dev/null +++ b/docs/api/torch-backend.md @@ -0,0 +1,8 @@ +# PyTorch backend (AMICATorchNG) + +The natural-gradient EM backend that reaches Fortran parity (Newton, exact-EM +mixture updates, symmetric-ZCA sphere, Jacobian log-likelihood). The +[`AMICA`](amica.md) interface delegates to this class; use it directly for +lower-level control. + +::: pyAMICA.AMICATorchNG diff --git a/docs/assets/figures/amica-algorithm-flow.svg b/docs/assets/figures/amica-algorithm-flow.svg new file mode 100644 index 0000000..5f83297 --- /dev/null +++ b/docs/assets/figures/amica-algorithm-flow.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + Center + Whiten + (ZCA sphere) + + E-step: + responsibilities + + M-step: source-density params (exact EM) + + unmixing W + (natural gradient -> Newton) + + converged? + + Best-iterate solution + no + yes + diff --git a/docs/assets/figures/gg-density-family.png b/docs/assets/figures/gg-density-family.png new file mode 100644 index 0000000..633e03e Binary files /dev/null and b/docs/assets/figures/gg-density-family.png differ diff --git a/docs/assets/figures/ica-mixing-geometry.png b/docs/assets/figures/ica-mixing-geometry.png new file mode 100644 index 0000000..e322250 Binary files /dev/null and b/docs/assets/figures/ica-mixing-geometry.png differ diff --git a/docs/assets/figures/ica-pipeline.svg b/docs/assets/figures/ica-pipeline.svg new file mode 100644 index 0000000..251c623 --- /dev/null +++ b/docs/assets/figures/ica-pipeline.svg @@ -0,0 +1,32 @@ + + + + + + + + + Sources + s(t) + + + + + mixing A + + Observations + x(t) + + + + + unmixing W + (estimated) + + Recovered + y(t) ≈ s(t) + + + + diff --git a/docs/assets/figures/kfactor-equivalence.png b/docs/assets/figures/kfactor-equivalence.png new file mode 100644 index 0000000..74798e2 Binary files /dev/null and b/docs/assets/figures/kfactor-equivalence.png differ diff --git a/docs/assets/figures/ll-convergence.png b/docs/assets/figures/ll-convergence.png new file mode 100644 index 0000000..c3b24d2 Binary files /dev/null and b/docs/assets/figures/ll-convergence.png differ diff --git a/docs/assets/figures/mixture-source-density.png b/docs/assets/figures/mixture-source-density.png new file mode 100644 index 0000000..9a2026e Binary files /dev/null and b/docs/assets/figures/mixture-source-density.png differ diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..8c0f846 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,12 @@ +# Changelog + +Release notes are published on the +[GitHub releases page](https://github.com/neuromechanist/pyAMICA/releases). + +## Unreleased + +- Documentation site (MkDocs Material) and GitHub Pages deployment. + +!!! note + A curated, versioned changelog will be maintained here starting with the + first public release. diff --git a/docs/concepts/how-amica-works.md b/docs/concepts/how-amica-works.md new file mode 100644 index 0000000..3016252 --- /dev/null +++ b/docs/concepts/how-amica-works.md @@ -0,0 +1,98 @@ +# How AMICA works + +AMICA fits its [generative model](what-is-amica.md) by **maximum likelihood**: +it searches for the unmixing matrices, biases, model weights, and source-density +parameters that make the observed data most probable. + +## The objective + +Given $N$ samples $\mathbf{x}(t)$, AMICA maximizes the total log-likelihood + +$$ +\mathcal{L} = \sum_{t=1}^{N} \log +\sum_{h=1}^{H} \gamma_h\,|\det \mathbf{W}_h| +\prod_{i=1}^{n} p_{hi}\!\big(y_{hi}(t)\big), +\qquad +y_{hi}(t) = \mathbf{w}_{hi}^{\top}\big(\mathbf{x}(t)-\mathbf{c}_h\big), +$$ + +over all parameters $\{\mathbf{W}_h, \mathbf{c}_h, \gamma_h, +\alpha_{hij}, \mu_{hij}, \beta_{hij}, \rho_{hij}\}$. The $|\det\mathbf{W}_h|$ +term is what keeps the unmixing matrices from collapsing to zero. + +## The algorithm + +Because the model has hidden assignments (which model, and which mixture +component, generated each sample), it is fit with +**expectation-maximization (EM)**, wrapped in a preprocessing and update loop. + +![AMICA pipeline: center and whiten the data, then iterate E-step and M-step (natural-gradient and Newton updates) until the log-likelihood converges.](../assets/figures/amica-algorithm-flow.svg){ width=760 } +/// caption +The AMICA fitting loop: preprocessing, then alternating E- and M-steps until the +log-likelihood converges. +/// + +### Preprocessing + +The data are centered (mean removed) and **whitened** with a symmetric (ZCA) +sphering matrix, matching the Fortran reference. Whitening removes second-order +correlations so the iterations only have to resolve higher-order structure. + +### E-step (responsibilities) + +Holding the parameters fixed, AMICA computes the posterior probabilities of the +hidden assignments for each sample: the probability that model $h$ generated it, +and, within each source, the probability that mixture component $j$ produced the +activation. These *responsibilities* are the expected sufficient statistics used +by the M-step. + +### M-step (parameter updates) + +Holding the responsibilities fixed, AMICA updates the parameters: + +- **Source-density parameters** ($\alpha, \mu, \beta, \rho$) are updated with the + exact expectation-maximization closed-form expressions (a digamma equation for + the shape $\rho$). +- **Unmixing matrices** are updated with the **natural gradient**, which + accounts for the geometry of the space of matrices and converges far faster + than the ordinary gradient: + +$$ +\Delta \mathbf{W}_h \;\propto\; +\big(\mathbf{I} - \langle\, \mathbf{g}(\mathbf{y})\,\mathbf{y}^{\top}\rangle\big)\, +\mathbf{W}_h, +$$ + + where $\langle\cdot\rangle$ is the responsibility-weighted average over samples + and $\mathbf{g}$ is the **score function** of the source density, + $g_i(y) = -\,\partial \log p_i(y)/\partial y$. For a single generalized + Gaussian component this is $g(y) = \rho\,\beta^{\rho}\,|y|^{\rho-1}\,\mathrm{sign}(y)$; + for a mixture it is the responsibility-weighted combination of its components' + scores. At the optimum the bracket vanishes, i.e. the sources are decorrelated + from their own scores, a statement of independence. + +- **Newton update.** Once the iterates are close, AMICA switches the unmixing + update to a **Newton step** (using the per-source curvature of the + likelihood), which sharpens convergence near the optimum. pyAMICA keeps this + step positive-definite for stability. + +### Convergence and the returned solution + +Each EM iteration increases the log-likelihood until it converges. Because the +learning-rate schedule is not strictly monotone, pyAMICA returns the +**highest-likelihood iterate** it visited (the *best-iterate* safeguard) rather +than the last one, and reports its likelihood as `final_ll_`. + +![Log-likelihood versus iteration on the real sample EEG, rising and converging.](../assets/figures/ll-convergence.png){ width=640 } +/// caption +Log-likelihood versus iteration on the bundled sample EEG: the objective rises +and converges toward the reference solution. +/// + +## Relationship to the Fortran reference + +Every M-step update in pyAMICA is derived to match the AMICA reference Fortran +implementation, and on real sample EEG the natural-gradient backend reaches the +same solution (log-likelihood and Hungarian-matched component correlation). See +[Validation & Parity](../guides/validation.md) for the acceptance criteria and +how cross-backend equivalence depends on data adequacy. diff --git a/docs/concepts/index.md b/docs/concepts/index.md new file mode 100644 index 0000000..15b2afb --- /dev/null +++ b/docs/concepts/index.md @@ -0,0 +1,16 @@ +# Background + +This section explains the ideas behind pyAMICA from the ground up, for readers +new to independent component analysis. + +- **[What is ICA?](what-is-ica.md)** — the blind source separation problem, the + linear mixing model, and why statistical independence and non-Gaussianity make + it solvable. +- **[What is AMICA?](what-is-amica.md)** — how AMICA extends ICA with *adaptive* + source densities (mixtures of generalized Gaussians) and *multiple* ICA models. +- **[How AMICA works](how-amica-works.md)** — the log-likelihood objective and + the expectation-maximization algorithm (natural-gradient and Newton updates) + that fits it. + +If you just want to run a decomposition, start with +[Getting Started](../getting-started.md) instead. diff --git a/docs/concepts/what-is-amica.md b/docs/concepts/what-is-amica.md new file mode 100644 index 0000000..ff8c2e6 --- /dev/null +++ b/docs/concepts/what-is-amica.md @@ -0,0 +1,89 @@ +# What is AMICA? + +Classical ICA algorithms assume a single, *fixed* shape for every source density +(for example a logistic or Laplacian distribution). Real signals rarely oblige: +some EEG sources are sharply peaked and heavy-tailed (super-Gaussian), others are +flatter (sub-Gaussian), and the mixing regime itself can change over time. +Assuming the wrong density biases the separation. + +**AMICA (Adaptive Mixture ICA)** removes that limitation with two "adaptive +mixture" ideas layered on top of ICA. + +## Idea 1: adaptive source densities + +Instead of fixing the source distribution, AMICA models each source as a +**mixture of generalized Gaussians** and learns its shape from the data. + +The generalized Gaussian density with location $\mu$, scale $\beta$, and shape +$\rho$ is + +$$ +q(y;\rho,\mu,\beta) = +\frac{\rho\,\beta}{2\,\Gamma(1/\rho)}\, +\exp\!\big(-\beta^{\rho}\,|y-\mu|^{\rho}\big). +$$ + +The shape parameter $\rho$ controls the tails: + +- $\rho = 2$ recovers the **Gaussian**, +- $\rho = 1$ is the **Laplacian** (peaky, super-Gaussian), +- $\rho < 2$ is **super-Gaussian** (heavier-tailed, more kurtotic), +- $\rho > 2$ is **sub-Gaussian** (flatter, platykurtic). + +![Generalized Gaussian density for several shape parameters, from super-Gaussian (peaky) through Gaussian to sub-Gaussian (flat).](../assets/figures/gg-density-family.png){ width=640 } +/// caption +The generalized Gaussian family spans super-Gaussian ($\rho<2$), Gaussian +($\rho=2$), and sub-Gaussian ($\rho>2$) shapes with a single parameter. +/// + +Each source density is a weighted mixture of $m$ such components, so it can take +on skewed and multimodal shapes: + +$$ +p_i(y) = \sum_{j=1}^{m} \alpha_{ij}\, +q\!\big(y;\rho_{ij},\mu_{ij},\beta_{ij}\big), +\qquad \sum_{j=1}^{m}\alpha_{ij}=1 . +$$ + +![A source density built as a weighted mixture of generalized Gaussian components.](../assets/figures/mixture-source-density.png){ width=640 } +/// caption +An adaptive source density (solid) as a weighted sum of generalized Gaussian +mixture components (dashed). +/// + +pyAMICA supports all five source-density families of the reference +implementation (generalized Gaussian is the default), plus an extended-Infomax +switcher that flips each source between super- and sub-Gaussian by the sign of +its kurtosis. + +## Idea 2: multiple ICA models + +A single unmixing matrix assumes one fixed mixing regime for the whole +recording. AMICA instead fits a **mixture of $H$ ICA models**, each with its own +unmixing matrix $\mathbf{W}_h$, bias $\mathbf{c}_h$, and source densities. A +per-sample *responsibility* softly assigns each time point to the model that +explains it best, so AMICA can capture non-stationarity and distinct regimes in +the data. + +The full generative model gives each observation the mixture likelihood + +$$ +p(\mathbf{x}) = \sum_{h=1}^{H} \gamma_h\, +|\det \mathbf{W}_h| +\prod_{i=1}^{n} p_{hi}\!\big(\mathbf{w}_{hi}^{\top}(\mathbf{x}-\mathbf{c}_h)\big), +$$ + +where $\gamma_h$ are the model weights ($\sum_h \gamma_h = 1$), +$\mathbf{w}_{hi}^{\top}$ is the $i$-th row of $\mathbf{W}_h$, and the +$|\det \mathbf{W}_h|$ (Jacobian) term accounts for the change of variables from +$\mathbf{x}$ to the sources. + +With $H = 1$ this reduces to ordinary ICA with adaptive source densities; that +single-model case is the default and reaches bit-level parity with the Fortran +reference. + +AMICA also supports **shared components** across models (merging near-collinear +sources) and **outlier rejection**. These are off by default. + +Next: [How AMICA works](how-amica-works.md), which fits this model by maximizing +its likelihood. diff --git a/docs/concepts/what-is-ica.md b/docs/concepts/what-is-ica.md new file mode 100644 index 0000000..01c14fa --- /dev/null +++ b/docs/concepts/what-is-ica.md @@ -0,0 +1,100 @@ +# What is ICA? + +## The cocktail-party problem + +Imagine several people talking at once in a room, recorded by several +microphones. Each microphone picks up a different *mixture* of all the voices. +**Independent Component Analysis (ICA)** is the art of recovering the individual +voices from the mixtures alone, without knowing who is speaking, where they +stand, or how the room blends the sound. This is *blind source separation*: +"blind" because we know neither the sources nor how they were mixed. + +The same problem appears in electroencephalography (EEG). Each scalp electrode +records a mixture of many overlapping brain and non-brain sources (cortical +patches, eye blinks, muscle activity). ICA unmixes the electrode signals into +maximally independent components, which often correspond to physiologically +meaningful sources. + +![ICA as blind source separation: independent sources are linearly mixed into the observed signals, and ICA estimates an unmixing matrix that recovers them.](../assets/figures/ica-pipeline.svg){ width=680 } +/// caption +ICA recovers unknown independent sources from their observed linear mixtures by +estimating an unmixing matrix. +/// + +## The linear mixing model + +ICA assumes the observations are an instantaneous linear mixture of the sources. +With $n$ sources and $n$ sensors, at each time point $t$: + +$$ +\mathbf{x}(t) = \mathbf{A}\,\mathbf{s}(t) +$$ + +where + +- $\mathbf{s}(t) \in \mathbb{R}^{n}$ are the unknown, mutually independent source + activations, +- $\mathbf{A} \in \mathbb{R}^{n \times n}$ is the unknown **mixing matrix** + (its columns are the sensor patterns, or "scalp maps," of each source), +- $\mathbf{x}(t) \in \mathbb{R}^{n}$ are the observed sensor signals. + +The goal is to estimate an **unmixing matrix** $\mathbf{W}$ that inverts the +mixing, so the recovered sources + +$$ +\mathbf{y}(t) = \mathbf{W}\,\mathbf{x}(t) \approx \mathbf{s}(t), +\qquad \mathbf{W} \approx \mathbf{A}^{-1}, +$$ + +are as close to the true sources as the data allow. + +## Why it works: independence and non-Gaussianity + +Two assumptions make this well-posed: + +1. **Statistical independence.** The sources are assumed mutually independent, so + their joint density factorizes, $p(\mathbf{s}) = \prod_i p_i(s_i)$. ICA looks + for the $\mathbf{W}$ that makes the outputs $y_i$ as independent as possible. +2. **Non-Gaussianity.** By the central limit theorem, a sum of independent + variables is "more Gaussian" than its parts, so a mixture looks more Gaussian + than the underlying sources. Recovering the sources is therefore equivalent to + finding the projections that are *maximally non-Gaussian*. A crucial + corollary: **Gaussian sources cannot be separated** (a rotation of + independent Gaussians is still independent Gaussians), so ICA requires at most + one Gaussian source. + +The geometry is easy to see with two sources. Independent sources fill an +axis-aligned region; mixing shears and rotates that region; ICA finds the +transform that restores the independent axes. + +![Two independent sources form an axis-aligned cloud; the mixing matrix shears it; the estimated unmixing matrix restores independent axes.](../assets/figures/ica-mixing-geometry.png){ width=720 } +/// caption +Left: two independent sources. Middle: the observed mixture is sheared and +rotated. Right: ICA recovers the independent axes (up to order and scale). +/// + +## Preprocessing: centering and whitening + +In practice the data are first **centered** (mean removed) and **whitened** +(also called *sphering*): linearly transformed so the channels are uncorrelated +and have unit variance. Whitening removes all second-order structure, reducing +the remaining ICA problem to finding an orthogonal rotation, which is both +faster and better conditioned. pyAMICA uses a symmetric (ZCA) whitening that +matches the Fortran reference. + +## What ICA cannot pin down + +Because $\mathbf{A}$ and $\mathbf{s}$ are both unknown, two ambiguities are +irreducible: + +- **Permutation.** The sources can be recovered in any order. +- **Scale and sign.** Each source's amplitude (and sign) is arbitrary, since a + scalar can move between a column of $\mathbf{A}$ and the corresponding source. + +These do not affect the usefulness of the components; they only mean component +*indices* and *scaling* are conventions, not ground truth. pyAMICA follows the +EEGLAB conventions for ordering and sign where it matters (see +[Validation & Parity](../guides/validation.md)). + +Next: [What is AMICA?](what-is-amica.md), which relaxes the fixed-source-density +assumption and adds multiple models. diff --git a/docs/development/contributing.md b/docs/development/contributing.md new file mode 100644 index 0000000..3672a92 --- /dev/null +++ b/docs/development/contributing.md @@ -0,0 +1,31 @@ +# Contributing + +Contributions are welcome. This page summarizes the development workflow; see the +full [CONTRIBUTING](https://github.com/neuromechanist/pyAMICA/blob/main/CONTRIBUTING.md) +and [Code of Conduct](https://github.com/neuromechanist/pyAMICA/blob/main/CODE_OF_CONDUCT.md) +in the repository root. + +## Development setup + +```bash +git clone https://github.com/neuromechanist/pyAMICA.git +cd pyAMICA +uv sync # install the project and dependencies +uv run pytest # run the test suite +``` + +## Conventions + +- **Environment:** UV only (no pip/conda/virtualenv for project management). +- **Lint/format:** `uv run ruff check --fix . && uv run ruff format .` +- **Tests:** real sample data and the Fortran binary only, never synthetic data + (see [Testing](testing.md)). +- **Correctness:** numerical parity with the Fortran reference is the + specification (see [Validation & Parity](../guides/validation.md)). +- **Commits:** atomic, concise messages, no emojis. + +## Reporting issues and getting help + +Please open an issue on the +[GitHub issue tracker](https://github.com/neuromechanist/pyAMICA/issues) for bug +reports, feature requests, and questions. diff --git a/docs/development/testing.md b/docs/development/testing.md new file mode 100644 index 0000000..9431fba --- /dev/null +++ b/docs/development/testing.md @@ -0,0 +1,24 @@ +# Testing + +## Real data only + +Correctness tests use real sample EEG and the reference Fortran binary shipped in +`pyAMICA/sample_data/`. Mocks, stubs, and synthetic data are not used as the +basis for correctness tests: no test is better than a fake passing test. + +## Running the suite + +```bash +uv run pytest # full suite +uv run pytest --cov # with coverage +uv run pytest pyAMICA/tests/torch_tests/ # PyTorch-vs-Fortran parity tests +``` + +## Layout + +- `pyAMICA/tests/` — end-to-end and interface tests. +- `pyAMICA/tests/torch_tests/` — natural-gradient backend parity, PDF families, + component sharing, float32 stability, and edge cases. +- `pyAMICA/tests/mlx_tests/` — MLX backend tests (Apple Silicon). +- `validate_implementations.py` — cross-implementation validation harness + (Hungarian component matching against Fortran). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..32de3e6 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,73 @@ +# Getting Started + +## Installation + +pyAMICA uses [UV](https://docs.astral.sh/uv/) for environment and dependency +management. + +### From source (development) + +```bash +git clone https://github.com/neuromechanist/pyAMICA.git +cd pyAMICA +uv sync # install the project and its dependencies into .venv +``` + +### As a dependency + +```bash +uv add pyAMICA # or: uv pip install pyAMICA +``` + +### Optional Apple-Silicon GPU backend + +The MLX backend is Apple-only and is therefore an optional extra; `import +pyAMICA` never requires it. + +```bash +uv pip install "pyAMICA[mlx]" # or: uv pip install mlx +``` + +## Quickstart + +The main entry point is the scikit-learn-style [`AMICA`](api/amica.md) class, +which wraps the natural-gradient EM backend. + +```python +import numpy as np +from pyAMICA import AMICA + +# X is (n_channels, n_samples); use real EEG/EMG rather than random data +# for a meaningful decomposition. +X = np.random.randn(32, 10000) + +amica = AMICA(n_models=1, n_mix=3) +amica.fit(X, max_iter=100) + +# Unmixed sources and the mixing/unmixing matrices +S = amica.transform(X) +A = amica.get_mixing_matrix(0) # mixing matrix for model 0 +W = amica.get_unmixing_matrix(0) # unmixing matrix for model 0 + +print("final log-likelihood:", amica.final_ll_) +``` + +!!! warning "Use `final_ll_`, not `ll_history_[-1]`" + With the best-iterate safeguard the returned parameters can be an earlier, + higher-likelihood iterate, so `final_ll_` is the log-likelihood of the + *returned* model. `ll_history_` is the true per-iteration trajectory and may + dip below its peak on a late overshoot. + +## Choosing a device and precision + +`AMICA` auto-selects a device. Because the backend computes in float64 for +Fortran parity and Apple MPS cannot represent float64, an auto-selected MPS +device is redirected to CPU; pass `device="mps"` with `dtype=torch.float32` to +run on MPS explicitly. See [Backends & Devices](guides/backends.md) for the full +matrix and performance guidance. + +## Next steps + +- [Backends & Devices](guides/backends.md) — CUDA / CPU / MLX and float32 vs float64. +- [Validation & Parity](guides/validation.md) — comparing against the Fortran reference. +- [API Reference](api/index.md) — full parameter and method documentation. diff --git a/docs/guides/backends.md b/docs/guides/backends.md new file mode 100644 index 0000000..708948c --- /dev/null +++ b/docs/guides/backends.md @@ -0,0 +1,58 @@ +# Backends & Devices + +pyAMICA ships one primary PyTorch backend behind the [`AMICA`](../api/amica.md) +interface, plus an optional Apple-GPU backend and a legacy NumPy reference. + +## Backends + +| Backend | Class | Role | +|---|---|---| +| PyTorch natural-gradient EM | [`AMICATorchNG`](../api/torch-backend.md) | **Default.** Fortran-parity backend; CUDA / CPU, and float32 on MPS. | +| MLX (Apple GPU) | `AMICAMLXNG` (`pyAMICA.mlx_impl`) | Optional Apple-Silicon GPU backend; float32 only. | +| NumPy reference | [`AMICA_NumPy`](../api/numpy-backend.md) | Legacy oracle + CLI; carries the same parity fixes. | + +The `AMICA` wrapper uses `AMICATorchNG`. The MLX backend is imported separately +(`from pyAMICA.mlx_impl import AMICAMLXNG`) so that `import pyAMICA` never +requires MLX. + +## Device selection + +`AMICA(device=...)` accepts `"cuda"`, `"cpu"`, `"mps"`, or `None` (auto): + +- **`None` (auto)** — selects CUDA if available, else CPU. An auto-selected MPS + device is redirected to CPU because the parity default is float64, which MPS + cannot represent. +- **`"cuda"`** — the bit-safe path for float64 Fortran parity on NVIDIA GPUs. +- **`"mps"`** — requires `dtype=torch.float32`. Note that PyTorch-MPS is not a + performance win for AMICA (see below); prefer the MLX backend on Apple hardware. + +## Precision: float64 vs float32 + +- **float64** — the default; required for Fortran-parity runs. CUDA float64 + agrees with the CPU log-likelihood to ~5 significant digits. +- **float32** — 5-19x faster and required on MPS/MLX, but ~7-significant-digit, + not float64-parity. Use it for exploratory or large-scale runs where exact + reference parity is not required. + +## Performance on real EEG + +Measured on real 70-channel EEG (see the project benchmarks and +`.context/issue-77/`): + +- On **Apple Silicon**, the **MLX backend is the GPU win** (~15-25 ms/iteration, + roughly flat from 16 to 70 channels), several times faster than torch-CPU and + faster than an RTX 4090 at EEG scale. **PyTorch-MPS does not win** (162-255 + ms/iteration, at or worse than CPU), so use MLX rather than `device="mps"` on + Apple hardware. +- On **NVIDIA**, CUDA float64 is the bit-safe path (~4.5x over a 16-thread CPU, + warmed); float32 is faster still. +- On **CPU**, intra-op threads are workload-limited; around 4 threads was the + sweet spot in the measured laptop sweep, with 8+ regressing. + +All backends agree on the log-likelihood to ~3 significant digits on real data. + +!!! note "Cross-backend equivalence and data adequacy" + Whether two backends recover the *same* independent components depends on how + well-determined the decomposition is (the data-adequacy factor + `k = frames / channels^2`). See [Validation & Parity](validation.md); the + full data-size sweep is being finalized. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 0000000..bf8a33f --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,11 @@ +# User Guide + +The user guide covers how to run pyAMICA in practice and how its results relate +to the reference implementation. + +- **[Backends & Devices](backends.md)** — the available compute backends + (PyTorch natural-gradient EM, optional MLX, legacy NumPy), device selection + (CUDA / CPU / MPS), float32 vs float64, and performance guidance on real EEG. +- **[Validation & Parity](validation.md)** — how correctness is defined as + parity with the Fortran reference, the validation harness, and how + cross-backend equivalence depends on data adequacy. diff --git a/docs/guides/validation.md b/docs/guides/validation.md new file mode 100644 index 0000000..992c458 --- /dev/null +++ b/docs/guides/validation.md @@ -0,0 +1,73 @@ +# Validation & Parity + +**Correctness in pyAMICA is defined as parity with the reference Fortran binary, +not merely as convergence.** A run is correct when it reproduces the Fortran +output within numerical tolerance. + +## The validation harness + +`validate_implementations.py` runs the implementations on real sample EEG, +matches components across implementations with the Hungarian algorithm, and +reports log-likelihood and per-component correlation. It always uses real sample +data and the Fortran binary, never synthetic data. + +## Single-model parity + +On real sample EEG the natural-gradient backend reaches Fortran's solution: + +- Log-likelihood ~ -3.40 (Fortran ~ -3.4018). +- Hungarian-matched component correlation ~0.997, clearing the >0.95 gate. + +The fixed source-density families are bit-exact against the literal Fortran +score/derivative expressions (~1e-15), and the backend converges to the binary's +solution within ~0.005 log-likelihood. + +## Multi-model equivalence + +Multi-model AMICA is not partition-identifiable, so exact partition parity with +Fortran is the wrong acceptance bar. The right test is whether the two +implementations sample the same distribution over solutions. On an ensemble of +real sample EEG runs, the pyAMICA-vs-Fortran partition cross-correlation +distribution is statistically equivalent to Fortran's own run-to-run +distribution. + +## Data adequacy and cross-backend equivalence + +Whether backends recover the *same* components depends on how well-determined the +decomposition is, captured by the data-adequacy factor: + +$$k = \frac{\text{frames}}{\text{channels}^2}$$ + +As `k` grows, cross-backend component equivalence rises toward 1.0; at the +rule-of-thumb minimum (`k` around 20-30) only the strongest components are +backend-reproducible, while the rest are under-determined and settle into +different but equally valid local optima (AMICA is non-convex). + +### Data-size sweep: equivalence versus k + +Holding channels fixed at 70 and increasing the number of frames (so `k` rises), +on real EEG (ds002718 sub-002), cross-backend IC equivalence climbs sharply and +then saturates once the decomposition is well-determined: + +![Cross-backend IC equivalence versus the data-adequacy factor k at 70 channels.](../assets/figures/kfactor-equivalence.png){ width=640 } +/// caption +Mean Hungarian-matched cross-backend |correlation| versus $k = \text{frames} / +\text{channels}^2$ (70 channels, 2000 iterations, native-Fortran and PyTorch-CUDA +float64/float32 backends). Equivalence saturates at ~0.98 once $k \geq 60$. +/// + +| frames | k | mean \|corr\| | components >0.95 | +|---|---|---|---| +| 73,500 | 15 | 0.911 | 55.2% | +| 147,000 | 30 | 0.929 | 56.7% | +| 294,000 | 60 | 0.982 | 90.0% | +| 490,000 | 100 | 0.983 | 94.8% | +| 747,750 | 152 | 0.982 | 92.4% | + +!!! note "The threshold is data-specific" + For this recording the equivalence knee falls **between k=30 and k=60**; below + it the backends settle into different (equally valid) local optima, above it + they recover the same components. Where that knee sits depends on the data + (signal-to-noise ratio, effective rank, source structure), so this is not a + universal value of `k`. The plateau is ~0.98 rather than 1.0 because of + intrinsic estimator spread and the float32 path, not a backend defect. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7ee1d91 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ +# pyAMICA + +Python (PyTorch) implementation of **Adaptive Mixture Independent Component +Analysis (AMICA)** that reproduces the results of the reference Fortran binary, +with GPU / Apple-GPU / CPU support. AMICA is a blind source separation algorithm +widely used for electroencephalography (EEG) and electromyography (EMG) source +decomposition. + +pyAMICA exposes a scikit-learn-style interface over a natural-gradient +expectation-maximization (EM) backend that matches the Fortran reference +(`amica15`) to within numerical tolerance: single-model log-likelihood and +Hungarian-matched component correlation both agree with Fortran on real EEG. + +## Why pyAMICA + +- **Fortran parity is the specification.** Correctness is defined as matching the + reference Fortran output within tolerance, not merely converging. See + [Validation & Parity](guides/validation.md). +- **Multiple backends, one API.** The default PyTorch natural-gradient EM backend + runs on CUDA, CPU, and (in float32) Apple MPS; an optional + [MLX backend](guides/backends.md) targets Apple-Silicon GPUs, and a legacy + NumPy reference is retained as an oracle. See [Backends & Devices](guides/backends.md). +- **Feature-complete against the reference.** Newton optimization, exact-EM + mixture updates, all five source-density families, component sharing, and + outlier rejection are ported and validated. + +## Quick links + +- [Getting Started](getting-started.md) — install and run your first decomposition. +- [Backends & Devices](guides/backends.md) — pick CUDA / CPU / MLX and float32 vs float64. +- [Validation & Parity](guides/validation.md) — how correctness is defined and checked. +- [API Reference](api/index.md) — the `AMICA` interface and the backend classes. + +!!! note "Precision and parity" + The natural-gradient backend computes in float64 for Fortran parity. Apple + MPS cannot represent float64, so parity runs use CPU or CUDA; float32 is + faster (and required on MPS/MLX) but is ~7-significant-digit, not + float64-parity. Use float64 for reference-parity runs. diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 0000000..4edf11a --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,21 @@ +// MathJax v3 configuration for pymdownx.arithmatex (generic mode). +// Re-typesets on MkDocs Material "instant" navigation. +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true, + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex", + }, +}; + +document$.subscribe(() => { + MathJax.startup.output.clearCache(); + MathJax.typesetClear(); + MathJax.texReset(); + MathJax.typesetPromise(); +}); diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..0e83a9e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,107 @@ +# pyAMICA documentation (MkDocs Material). +# Deployed to GitHub Pages by .github/workflows/docs.yml. Once the repo lives at +# github.com/sccn/pyAMICA, its project Pages inherit the sccn org Pages custom +# domain (eeglab.org) at the /pyAMICA subpath, so the site is served at +# https://eeglab.org/pyAMICA/. Links are relative so the same build also works at +# https://neuromechanist.github.io/pyAMICA/ before the transfer. + +site_name: pyAMICA Documentation +site_url: https://eeglab.org/pyAMICA/ +site_description: Python (PyTorch) implementation of Adaptive Mixture ICA (AMICA) with Fortran parity and GPU/MPS/CPU support. +site_author: Seyed Yahya Shirazi + +repo_url: https://github.com/neuromechanist/pyAMICA/ +repo_name: neuromechanist/pyAMICA +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.indexes + - navigation.top + - navigation.tracking + - navigation.instant + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + - toc.integrate + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.arithmatex: # LaTeX math via MathJax (see extra_javascript below) + generic: true + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + toc_depth: 3 + - attr_list + - md_in_html + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_source: true + show_root_heading: true + heading_level: 2 + docstring_style: numpy + members_order: source + filters: ["!^_"] + - git-revision-date-localized: + enable_creation_date: true + fallback_to_build_date: true + +# MathJax (LaTeX math) — see docs/javascripts/mathjax.js for the arithmatex hookup +extra_javascript: + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +nav: + - Home: index.md + - Getting Started: getting-started.md + - Background: + - Overview: concepts/index.md + - What is ICA?: concepts/what-is-ica.md + - What is AMICA?: concepts/what-is-amica.md + - How AMICA works: concepts/how-amica-works.md + - User Guide: + - Overview: guides/index.md + - Backends & Devices: guides/backends.md + - Validation & Parity: guides/validation.md + - API Reference: + - Overview: api/index.md + - AMICA: api/amica.md + - PyTorch backend: api/torch-backend.md + - NumPy backend: api/numpy-backend.md + - Development: + - Contributing: development/contributing.md + - Testing: development/testing.md + - Changelog: changelog.md + +copyright: Copyright © 2023-2025 Seyed Yahya Shirazi diff --git a/pyproject.toml b/pyproject.toml index 0e333ce..7c0d732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,13 @@ dependencies = [ # pyAMICA[mlx]`. `import pyAMICA` never requires it (mlx_impl is imported lazily). [project.optional-dependencies] mlx = ["mlx>=0.32"] +# Documentation stack (MkDocs Material + mkdocstrings). Built and deployed to +# GitHub Pages by .github/workflows/docs.yml via `uv sync --extra docs`. +docs = [ + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.26", + "mkdocs-git-revision-date-localized-plugin>=1.2", +] [project.urls] Homepage = "http://github.com/neuromechanist/pyAMICA" diff --git a/uv.lock b/uv.lock index 3464648..de9f8ab 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,110 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[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" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -289,6 +393,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] +[[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 = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + +[[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" @@ -405,6 +563,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, ] +[[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 = "markupsafe" version = "3.0.3" @@ -522,6 +689,149 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/95/1d36bddf2b7e2692c1540e78a6e5bc88bc1496b137e3e35a611f91b65ac3/matplotlib-3.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:652fb5696271d4c50f196d22a5ff4f8e4444c74f847423570d7dc0aa2bbd0159", size = 9209226, upload-time = "2026-06-12T02:29:07.033Z" }, ] +[[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-git-revision-date-localized-plugin" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "gitpython" }, + { name = "mkdocs" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/99/8067eb7d1652767ee8e5474010647dd5a8e464e0ca8c783b5cac135a2043/mkdocs_git_revision_date_localized_plugin-1.5.3.tar.gz", hash = "sha256:873444b54cab4d47c69bd6e85da05ef5fbe81fee27e64508114c46a0e4f81e37", size = 451961, upload-time = "2026-06-01T08:32:09.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/d0/cbe85158dc091219fd5134bf6d724d30b1f2005ee1d0dabaaa41416bee78/mkdocs_git_revision_date_localized_plugin-1.5.3-py3-none-any.whl", hash = "sha256:cd96e432de6a7e59b31c7041574b22f84179c8636835419ff458877ecfaaaf05", size = 26156, upload-time = "2026-06-01T08:32:07.765Z" }, +] + +[[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.5" +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/46/8c/05934c546eb38321e5f24e786b24c7a0dc5f2b35968f6e04dce9cfbb738d/mkdocstrings-1.0.5.tar.gz", hash = "sha256:3a9528223afd6bf459ea85541b600ef3542826bbeb4ab534553f4b3c4e6e5e48", size = 100318, upload-time = "2026-07-10T10:49:10.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/82/2a2d44f2c09563d321ab2690835f066267fe000623c90c0caf44e296cbbb/mkdocstrings-1.0.5-py3-none-any.whl", hash = "sha256:150eff6e8c370cc8d2ba4e452ece1275fe83ad34d8cc5884a89a0ed141618c49", size = 35543, upload-time = "2026-07-10T10:49:08.689Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, +] + [[package]] name = "mlx" version = "0.32.0" @@ -793,6 +1103,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[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 = "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 = "pillow" version = "12.3.0" @@ -864,6 +1192,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, ] +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -888,6 +1225,11 @@ dependencies = [ ] [package.optional-dependencies] +docs = [ + { name = "mkdocs-git-revision-date-localized-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, +] mlx = [ { name = "mlx" }, ] @@ -904,6 +1246,9 @@ dev = [ requires-dist = [ { name = "json5" }, { name = "matplotlib" }, + { name = "mkdocs-git-revision-date-localized-plugin", marker = "extra == 'docs'", specifier = ">=1.2" }, + { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.26" }, { name = "mlx", marker = "extra == 'mlx'", specifier = ">=0.32" }, { name = "numpy" }, { name = "scipy" }, @@ -911,7 +1256,7 @@ requires-dist = [ { name = "torch", specifier = ">=2.12.1" }, { name = "tqdm" }, ] -provides-extras = ["mlx"] +provides-extras = ["mlx", "docs"] [package.metadata.requires-dev] dev = [ @@ -930,6 +1275,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 = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -994,6 +1352,79 @@ 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/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" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[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 = "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 = "ruff" version = "0.15.20" @@ -1088,6 +1519,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -1184,3 +1624,45 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3 wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[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 = "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/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" }, +]