Initial release: E1 genus probe package moved out of the E01 workflow
- src/custom_models torch modules, store, training, metrics, runner and CLI (package-relative imports for pip installability) - test suite (49 tests) + conftest with fast synthetic-store fixtures - uv-managed dev env (pyproject + CPU torch index), hatchling build - README: uv for the package, pip install into conda envs for the workflow
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.venv/
|
||||
dist/
|
||||
build/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
@@ -0,0 +1,261 @@
|
||||
# custom_models — E1 genus probe CLI
|
||||
|
||||
Torch models and a generalised train/validate/test wrapper for the E1
|
||||
genus-information-ceiling experiment (GATE ZERO / torch model spec).
|
||||
`custom-models` is its own pip-installable package (this repo, `src/`
|
||||
layout, built with hatchling).
|
||||
|
||||
## Install
|
||||
|
||||
Dev/build environment is **uv** (much faster solver + lockfile):
|
||||
|
||||
```bash
|
||||
uv sync --extra dev # venv at .venv with CPU torch from the uv index
|
||||
uv run pytest # run the test suite
|
||||
uv run custom-models build --arch cnn --budget 1000000 --n-classes 11
|
||||
```
|
||||
|
||||
Import into the main workflow's **conda** envs — torch/numpy/polars come
|
||||
from conda there and already satisfy the requirements, so pip adds only
|
||||
this package:
|
||||
|
||||
```bash
|
||||
conda run -n e1-torch-cpu pip install /path/to/custom_models
|
||||
conda run -n e1-torch-cpu python -m custom_models <subcommand> ...
|
||||
# or with pod5 support for the `store` subcommand:
|
||||
conda run -n e1-torch-cpu pip install "/path/to/custom_models[pod5]"
|
||||
```
|
||||
|
||||
Run as:
|
||||
|
||||
```bash
|
||||
python -m custom_models <subcommand> ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Subcommand | Purpose |
|
||||
|-------------|---------|
|
||||
| `build` | Resolve a nominal parameter budget into a realised probe geometry; prints JSON. |
|
||||
| `store` | Build a signal store either from pod5 + labels parquet or a synthetic duty-cycle corpus. |
|
||||
| `train` | One training run on a store; optionally a shuffled-label control. Writes a run directory. |
|
||||
| `validate` | Evaluate a run's checkpoint on its validation split (bootstrap CIs). |
|
||||
| `test` | Evaluate a run's checkpoint on its test split; optionally attach a trap (out-of-bank) store. |
|
||||
| `gate` | E1-v2 gate verdict: compare candidate run dirs against a control run. |
|
||||
| `configs` | Round-trip helper: load a config JSON and re-save it (`load_config(save_config(c)) == c`). |
|
||||
|
||||
Every subcommand seeds before doing anything; all exit 0 on success, 1 on
|
||||
usage/runtime errors (message on stderr).
|
||||
|
||||
---
|
||||
|
||||
## Input: the labels parquet
|
||||
|
||||
`store` (pod5 mode) and `extract_store` require a labels table (parquet)
|
||||
with these columns (order-free; `LABEL_COLUMNS` in `store.py`):
|
||||
|
||||
| Column | Type | Role |
|
||||
|--------------|--------|------|
|
||||
| `read_id` | str | ONT read id — the join key against the pod5 files. **Must be unique** (duplicates raise `ValueError`). |
|
||||
| `genus` | str | Class label used by the probe. |
|
||||
| `species` | str | Fine-grained secondary label (carried through to the manifest; unused by training). |
|
||||
| `ref_name` | str | Reference sequence name the read was aligned to. |
|
||||
| `identity` | float | Alignment identity (e.g. minimap2 `id%`), for provenance/filtering elsewhere. |
|
||||
| `aligned_len`| int | Aligned reference length. |
|
||||
| `query_len` | int | Read length (basis for the coverage-style ratios). |
|
||||
| `source_run` | str | Sequencing run the read came from — enables the `holdout_runs` split mode. |
|
||||
|
||||
Extra columns are ignored. The genus set actually trained on is chosen at
|
||||
run time with `train --genera A,B,C` (default: every genus in the store).
|
||||
|
||||
## Input: the pod5 files
|
||||
|
||||
`store --pod5 FILE [FILE ...] --labels LABELS.parquet --out STORE_DIR`.
|
||||
Windows are cut per read: skip `--skip-head` samples (adapter/mux
|
||||
artefacts), then take up to `--max-windows-per-read` contiguous fixed
|
||||
windows of `--window-samples` samples (default 12,000 ≈ 2.4 s at 5 kHz).
|
||||
Reads shorter than `--min-read-samples` are dropped. Each window is
|
||||
normalised (`--norm median_iqr`, the amplitude-erasing default) and
|
||||
written float16 (`--dtype`).
|
||||
|
||||
## Output: the signal store layout
|
||||
|
||||
```text
|
||||
STORE_DIR/
|
||||
├── store_meta.json # extraction stats, counts, dropped-read accounting
|
||||
├── manifest.parquet # per-window table:
|
||||
│ # window_id, read_id, genus, species, source_run,
|
||||
│ # window_index, n_samples
|
||||
└── shard_%05d.npy # float16 windows, one file per --shard-windows windows
|
||||
```
|
||||
|
||||
Both training paths (real + synthetic) read through `SignalStore`, which
|
||||
validates the manifest columns on open.
|
||||
|
||||
---
|
||||
|
||||
## `train` and the option model
|
||||
|
||||
`train` accepts a previous run's `config.json` as `--config` base;
|
||||
explicit flags override it. Resolution order per field:
|
||||
|
||||
```
|
||||
explicit flag > base config value > spec default
|
||||
```
|
||||
|
||||
This makes variants (ablations, holdout robustness, retraining,
|
||||
relocation of the run dir) reproducible by editing one or two flags.
|
||||
|
||||
### Model parameters (`models.py` role)
|
||||
|
||||
| Flag | Default | Role |
|
||||
|------|---------|------|
|
||||
| `--arch` | `cnn` | Encoder family: `cnn` (patch-embed + residual conv blocks + gated mean pool) or `linatt` (same conv stack tokenised, linear attention blocks, mean pool). |
|
||||
| `--budget` | 1,000,000 | Nominal trainable-parameter budget. `build_probe` searches layer widths up to `budget * (1 + --budget-tol)` for ceilings well above the arch's table count. `build`, then the basis for the deterministic search over deeper/wider stacks. |
|
||||
| `--stride` | 4 | Patch stride in signal samples (token pitch), 4 or 8; also fixes the patch length (4 samples / patch). Smaller stride → more tokens → finer temporal resolution, more compute. |
|
||||
| `--d-embed` | 256 | Pooled embedding width the encoder terminates in. |
|
||||
| `--budget-tol` | 0.10 | Relative tolerance; realised params must be `<= (1+tol) × budget`. |
|
||||
| `--n-heads` | 4 | Attention head count for `linatt` (recorded for exact checkpoint rehydration). |
|
||||
|
||||
Realised geometry (`d_model`, `n_layers`, `params_realised`) is filled by
|
||||
`build_probe` and written into the run's `config.json`; it is
|
||||
rehydrated, never guessed, when a saved checkpoint is loaded.
|
||||
|
||||
### Data parameters
|
||||
|
||||
| Flag | Default | Role |
|
||||
|------|---------|------|
|
||||
| `--genera` | all in store | Comma-separated class whitelist (e.g. `Bacillus,Listeria`). Genera absent from the store drop out with a warning — the run id records the realised count `gN`. |
|
||||
| `--val-frac` | 0.10 | Fraction of reads (per genus) used for validation (splits are read-level so windows of one read never straddle a split). |
|
||||
| `--test-frac` | 0.10 | Fraction of reads for test. |
|
||||
| `--holdout-runs` | off | Split by `source_run` instead of by read (robustness row: unseen instrument/run). |
|
||||
| `--min-test-windows` | 300 | Per-genus floor for test windows; `make_splits` refuses to build a split that would starve a genus rather than silently evaluating on nothing. |
|
||||
| `--data-seed` | 0 | Split permutation seed (independent of the training seed). |
|
||||
|
||||
### Training parameters
|
||||
|
||||
| Flag | Default | Role |
|
||||
|------|---------|------|
|
||||
| `--batch` | 256 | Optimiser batch size (read windows). Train loader uses `drop_last=True`. |
|
||||
| `--lr` | 3e-4 | Peak AdamW learning rate; the warmup-cosine schedule holds returns to 0 by `--epochs`. |
|
||||
| `--weight-decay` | 0.01 | Decoupled decay; not applied to norm weights or biases. |
|
||||
| `--epochs` | 40 | Maximum epochs (early stopping may exit sooner). |
|
||||
| `--warmup` | 0.05 | Fraction of total optimiser steps spent ramping LR 0→peak. |
|
||||
| `--smoothing` | 0.0 | Cross-entropy label smoothing. |
|
||||
| `--amp` | `bf16` | Autocast policy: `bf16` where supported, `fp16` (CUDA + GradScaler) or `off` (fp32; also forced on CPU). Falls back silently to fp32 when unsupported. |
|
||||
| `--grad-clip` | 1.0 | L2 gradient-norm clip. |
|
||||
| `--metric` | `val_recall_macro` | Early-stop selection metric (the headline gate metric; `val_loss` is the alternative). |
|
||||
| `--patience` | 5 | Consecutive epochs without improvement before stopping. |
|
||||
| `--workers` | 4 | DataLoader worker processes (workers are seeded; Linux pins the `fork` start method because 3.14 default `forkserver` is broken here). |
|
||||
| `--device` | auto | `cuda` → `mps` → `cpu` resolution; or an explicit torch device string. |
|
||||
| `--seed` | 0 | Training seed; every stochastic step is seeded from it. |
|
||||
| `--deterministic` | on | Ensure kernels choose inverse-deterministic ops. |
|
||||
|
||||
### Run bookkeeping
|
||||
|
||||
| Flag | Default | Role |
|
||||
|------|---------|------|
|
||||
| `--store` / `--out-dir` | required (or via base config) | Where the signal store lives and where the run directory is created. |
|
||||
| `--config` | — | Base `RunConfig` JSON (a previous run's `config.json`). |
|
||||
| `--run-id` | derived | If empty: `{arch}_{budget_M}M_s{stride}_g{n_genera}` (control adds `-shuf`). |
|
||||
| `--stage` | `arch_ladder` | Stage tag: `arch_ladder`, `windows_ablation`, `robustness`, `control`, `extra`. |
|
||||
| `--control` | off | Marks the run as a shuffled-label control (overrides `--stage`). |
|
||||
| `--notes` | — | Free-text note stored verbatim in `config.json`. |
|
||||
|
||||
### Run directory layout
|
||||
|
||||
```text
|
||||
OUT_DIR/
|
||||
└── RUN_ID/
|
||||
├── config.json # realised RunConfig (round-trips exactly)
|
||||
├── ckpt.pt # best-epoch weights (CPU tensors)
|
||||
├── history.parquet # per-epoch: epoch, lr, train/val loss, val acc, recall, seconds
|
||||
├── val_report.json # written by `validate`
|
||||
├── report.json # written by `test`
|
||||
├── trap_report.json # written by `test` (when --trap-store is given)
|
||||
└── gate_verdict.json # written by `gate` (common parent of the runs)
|
||||
```
|
||||
|
||||
### Control runs
|
||||
|
||||
`train --control` permutes the read-to-genus mapping across the selected
|
||||
pool (seeded, preserving the genus multiset exactly); every window
|
||||
inherits its read's new label. Applied *before* splitting, so the
|
||||
control's splits and labels live consistently in the permuted world and
|
||||
a trained control should land at chance accuracy (1/n_classes). Control
|
||||
runs exist so the gate has a leakage-litmus baseline.
|
||||
|
||||
---
|
||||
|
||||
## `validate` / `test` / `gate`
|
||||
|
||||
```bash
|
||||
python -m custom_models validate --run-dir OUT_DIR/RUN_ID [--store S] [--n-boot 10000]
|
||||
python -m custom_models test --run-dir OUT_DIR/RUN_ID [--store S] [--trap-store T] [--n-boot]
|
||||
python -m custom_models gate --runs RUN_A RUN_B ... --control OUT_DIR/CNN...shuf [--chance-tol 1.5]
|
||||
```
|
||||
|
||||
`--store` points at a different signal store than the one recorded in
|
||||
`config.json` (e.g. evaluating a synthetic-trained probe on real data —
|
||||
cross-run transfer); by default the recorded store is re-used.
|
||||
|
||||
`--trap-store` runs the probe against an out-of-class "trap" set during
|
||||
`test`: the closed-set open-set probe reports max-softmax-probability
|
||||
(msp) calibration, threshold `tau`, and trap-vs-known msp separation
|
||||
(FPR/FNR at the chosen `tau`).
|
||||
|
||||
`gate` evaluates the closed- and open-set reports of each candidate run
|
||||
against the E1-v2 gate rules (chance = 1/n_classes from the control's
|
||||
config):
|
||||
|
||||
- The **control** must itself be at chance (recall ≤ `--chance-tol` ×
|
||||
chance); a control above that is INVALID (suspected label leakage) and
|
||||
halts the interpretation.
|
||||
- **PASS**: test recall-macro ≥ 0.90 with ≤ 5M realised params and trap
|
||||
FPR at tau ≤ 0.05.
|
||||
- **MARGINAL-TRAP**: recall ≥ 0.90 at ≤ 5M but failing trap FPR — adopt
|
||||
per-class/margin thresholding.
|
||||
- **MARGINAL**: recall in [0.70, 0.90) at ≤ 5M, or 0.90 only at ≤ 20M
|
||||
with the trap criterion satisfied.
|
||||
- **FAIL**: anything else. Missing trap evaluation degrades PASS to
|
||||
MARGINAL (the open-set criterion cannot be confirmed).
|
||||
|
||||
The verdict (`gate_verdict.json`) is written to the runs' common parent
|
||||
directory.
|
||||
|
||||
---
|
||||
|
||||
## Typical workflow
|
||||
|
||||
```bash
|
||||
# 1. store
|
||||
python -m custom_models store --pod5 run1.pod5 run2.pod5 \
|
||||
--labels reads.parquet --out data/store_v1
|
||||
|
||||
# 2. build (check geometry before training)
|
||||
python -m custom_models build --arch cnn --budget 1000000 --n-classes 6
|
||||
|
||||
# 3. tune the class ladder (train each stage on the same store)
|
||||
for g in 3 4 5 6; do
|
||||
python -m custom_models train --store data/store_v1 --out-dir runs \
|
||||
--arch cnn --budget 1000000 --genera $(head -n $g genera.txt | paste -sd,) \
|
||||
--stage arch_ladder --notes "information ceiling"
|
||||
done
|
||||
|
||||
# 4. shuffled-label control
|
||||
python -m custom_models train --store data/store_v1 --out-dir runs \
|
||||
--arch cnn --budget 1000000 --control
|
||||
|
||||
# 5. evaluate + gate (candidates need a `report.json` from `test`)
|
||||
python -m custom_models test --run-dir runs/cnn_1.0M_s4_g6 --trap-store data/trap
|
||||
python -m custom_models gate --runs runs/cnn_1.0M_s4_g6 \
|
||||
--control runs/cnn_1.0M_s4_g6-shuf
|
||||
```
|
||||
|
||||
Run a subset via:
|
||||
|
||||
```bash
|
||||
conda run -n e1-torch-cpu python -m custom_models ...
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.25"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "custom-models"
|
||||
version = "0.1.0"
|
||||
description = "E1 genus probe: torch models and train/validate/test wrapper"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "Tom Kasper" }]
|
||||
dependencies = [
|
||||
"numpy>=2.0,<3",
|
||||
"polars>=1.0",
|
||||
"torch>=2.6",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
pod5 = ["pod5"]
|
||||
dev = ["pytest>=8", "ruff>=0.8", "pod5"]
|
||||
|
||||
[project.scripts]
|
||||
custom-models = "custom_models.cli:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/custom_models"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "D", "B", "RUF", "TRY", "I"]
|
||||
ignore = [
|
||||
"TRY003", # long messages are the run-artifact contract here
|
||||
"TRY004", # ValueError (not TypeError) on config/type misuse: deliberate
|
||||
"TRY300",
|
||||
"B905", # explicit strict in zip() is noisy for internal iteration
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["D103", "D401"]
|
||||
|
||||
# UV: dev/build environment resolves torch from the CPU wheel index so the
|
||||
# venv stays small. pip installs into the workflow's conda envs ignore this
|
||||
# (torch comes from conda there and satisfies the requirement).
|
||||
[tool.uv]
|
||||
index-strategy = "unsafe-best-match"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pytorch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
torch = { index = "pytorch-cpu" }
|
||||
@@ -0,0 +1,78 @@
|
||||
"""E1 genus probe: torch models, training wrapper and evaluation (v0.1.0).
|
||||
|
||||
Public API re-exports the pieces a run needs: configs (``config``),
|
||||
determinism (``seed``), the signal store (``store``), splits and loaders
|
||||
(``data``), the torch modules and budget resolver (``models``), the
|
||||
training loop and inference (``train``), metrics (``metrics``), run
|
||||
orchestration and the gate (``runner``) and the CLI (``cli``). The
|
||||
normative contracts live in ``planning/design_docs/preliminary_
|
||||
experiments/E1_torch_model_spec.md`` and ``E1_v2_eleven_taxa_redesign.md``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .config import (
|
||||
DataConfig,
|
||||
ExtractConfig,
|
||||
ModelConfig,
|
||||
RunConfig,
|
||||
TrainConfig,
|
||||
load_config,
|
||||
run_id_for,
|
||||
save_config,
|
||||
)
|
||||
from .data import ProbeDataset, Splits, make_splits, permute_read_labels
|
||||
from .models import (
|
||||
ConvEncoder,
|
||||
GenusProbe,
|
||||
LinearAttentionEncoder,
|
||||
SignalPatchEmbed,
|
||||
build_probe,
|
||||
count_params,
|
||||
)
|
||||
from .seed import make_generator, seed_everything, worker_init_fn
|
||||
from .store import SignalStore, extract_store, load_labels, synthetic_store
|
||||
from .train import (
|
||||
TrainResult,
|
||||
build_optimizer,
|
||||
embed_probe,
|
||||
predict,
|
||||
select_device,
|
||||
train_probe,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"ConvEncoder",
|
||||
"DataConfig",
|
||||
"ExtractConfig",
|
||||
"GenusProbe",
|
||||
"LinearAttentionEncoder",
|
||||
"ModelConfig",
|
||||
"ProbeDataset",
|
||||
"RunConfig",
|
||||
"SignalPatchEmbed",
|
||||
"SignalStore",
|
||||
"Splits",
|
||||
"TrainConfig",
|
||||
"TrainResult",
|
||||
"build_optimizer",
|
||||
"build_probe",
|
||||
"count_params",
|
||||
"embed_probe",
|
||||
"extract_store",
|
||||
"load_config",
|
||||
"load_labels",
|
||||
"make_generator",
|
||||
"make_splits",
|
||||
"permute_read_labels",
|
||||
"predict",
|
||||
"run_id_for",
|
||||
"save_config",
|
||||
"seed_everything",
|
||||
"select_device",
|
||||
"synthetic_store",
|
||||
"train_probe",
|
||||
"worker_init_fn",
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Module entry point: ``python -m custom_models <subcommand>``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,571 @@
|
||||
"""Generalised train/validate/test CLI for the E1 genus probe.
|
||||
|
||||
Subcommands: ``build`` (resolve a budget), ``store`` (build a signal
|
||||
store from pod5+labels or a synthetic corpus), ``train`` (a full run,
|
||||
optionally a shuffled-label control), ``validate`` / ``test`` (evaluate a
|
||||
run directory's checkpoint, ``test`` optionally attaching a trap store),
|
||||
``gate`` (verdict over run dirs) and ``configs`` (config round-trip).
|
||||
Every subcommand seeds first (GATE ZERO §2.3); each returns exit code 0
|
||||
on success and 1 on usage/runtime errors. ``train`` accepts a previous
|
||||
run's ``config.json`` as a base with any flag overriding it, so variants
|
||||
(windows ablation, holdout robustness, retraining) stay reproducible.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .config import (
|
||||
DataConfig,
|
||||
ExtractConfig,
|
||||
ModelConfig,
|
||||
RunConfig,
|
||||
TrainConfig,
|
||||
load_config,
|
||||
save_config,
|
||||
)
|
||||
from .runner import evaluate_gate, evaluate_test, evaluate_val, train_run
|
||||
|
||||
|
||||
def _add_model_args(parser: argparse.ArgumentParser, required: bool) -> None:
|
||||
"""Register the model-request flags on a subparser.
|
||||
|
||||
Args:
|
||||
parser: Target subparser.
|
||||
required: Whether ``--arch``/``--budget`` must be given (the
|
||||
``build`` command demands them; ``train`` falls back to
|
||||
defaults/base-config values).
|
||||
|
||||
"""
|
||||
parser.add_argument("--arch", choices=["cnn", "linatt"], required=required)
|
||||
parser.add_argument("--budget", type=int, required=required)
|
||||
parser.add_argument("--stride", type=int, default=None)
|
||||
parser.add_argument("--d-embed", type=int, default=None)
|
||||
parser.add_argument("--budget-tol", type=float, default=None)
|
||||
parser.add_argument("--n-heads", type=int, default=None)
|
||||
|
||||
|
||||
def _add_data_args(parser: argparse.ArgumentParser) -> None:
|
||||
"""Register the split-configuration flags on a subparser.
|
||||
|
||||
All default to ``None`` so only explicitly passed flags override a
|
||||
base config (tri-state booleans use ``default=None``).
|
||||
|
||||
Args:
|
||||
parser: Target subparser.
|
||||
|
||||
"""
|
||||
parser.add_argument("--genera", default=None)
|
||||
parser.add_argument("--val-frac", type=float, default=None)
|
||||
parser.add_argument("--test-frac", type=float, default=None)
|
||||
parser.add_argument("--holdout-runs", action="store_true", default=None)
|
||||
parser.add_argument("--min-test-windows", type=int, default=None)
|
||||
parser.add_argument("--data-seed", type=int, default=None)
|
||||
|
||||
|
||||
def _add_train_args(parser: argparse.ArgumentParser) -> None:
|
||||
"""Register the training-semantics flags on a subparser.
|
||||
|
||||
Args:
|
||||
parser: Target subparser.
|
||||
|
||||
"""
|
||||
parser.add_argument("--batch", type=int, default=None)
|
||||
parser.add_argument("--lr", type=float, default=None)
|
||||
parser.add_argument("--weight-decay", type=float, default=None)
|
||||
parser.add_argument("--epochs", type=int, default=None)
|
||||
parser.add_argument("--warmup", type=float, default=None)
|
||||
parser.add_argument("--smoothing", type=float, default=None)
|
||||
parser.add_argument("--amp", choices=["off", "bf16", "fp16"], default=None)
|
||||
parser.add_argument("--grad-clip", type=float, default=None)
|
||||
parser.add_argument(
|
||||
"--metric", choices=["val_recall_macro", "val_loss"], default=None
|
||||
)
|
||||
parser.add_argument("--patience", type=int, default=None)
|
||||
parser.add_argument("--workers", type=int, default=None)
|
||||
parser.add_argument("--device", default=None)
|
||||
parser.add_argument("--seed", type=int, default=None)
|
||||
parser.add_argument("--deterministic", action="store_true", default=None)
|
||||
|
||||
|
||||
def _pick(value: Any, base: Any, default: Any) -> Any:
|
||||
"""Three-way option resolution: flag > base config > default.
|
||||
|
||||
Args:
|
||||
value: The parsed CLI flag (``None`` when not passed).
|
||||
base: The corresponding value from a base config (``None``
|
||||
without one).
|
||||
default: The spec default used last.
|
||||
|
||||
Returns:
|
||||
The first non-``None`` of the three.
|
||||
|
||||
"""
|
||||
if value is not None:
|
||||
return value
|
||||
if base is not None:
|
||||
return base
|
||||
return default
|
||||
|
||||
|
||||
def _parse_genera(raw: str | None) -> tuple[str, ...]:
|
||||
"""Parse a comma-separated genus list.
|
||||
|
||||
Args:
|
||||
raw: e.g. ``"Bacillus,Listeria,Staphylococcus"``; ``None`` means
|
||||
no restriction (use every genus in the store).
|
||||
|
||||
Returns:
|
||||
The trimmed non-empty names as a tuple.
|
||||
|
||||
Raises:
|
||||
ValueError: If the string contains no non-empty names.
|
||||
|
||||
"""
|
||||
if raw is None:
|
||||
return ()
|
||||
genera = tuple(g.strip() for g in raw.split(",") if g.strip())
|
||||
if not genera:
|
||||
raise ValueError("--genera parsed to an empty list")
|
||||
return genera
|
||||
|
||||
|
||||
def _run_config_from_args(args: argparse.Namespace) -> RunConfig:
|
||||
"""Assemble a :class:`RunConfig` from flags plus optional base config.
|
||||
|
||||
Resolution order per field: explicit flag, then the base config's
|
||||
value (when ``--config`` was given), then the spec default.
|
||||
``--control`` or ``--stage control`` marks the run as the
|
||||
shuffled-label control.
|
||||
|
||||
Args:
|
||||
args: Parsed ``train`` arguments.
|
||||
|
||||
Returns:
|
||||
The fully resolved run configuration.
|
||||
|
||||
Raises:
|
||||
ValueError: If the base file is not a ``RunConfig`` or neither
|
||||
flags nor base supply ``--store`` / ``--out-dir``.
|
||||
|
||||
"""
|
||||
base: RunConfig | None = None
|
||||
if args.config:
|
||||
base = load_config(args.config)
|
||||
if not isinstance(base, RunConfig):
|
||||
raise ValueError(f"{args.config} does not hold a RunConfig")
|
||||
base_model = base.model if base else None
|
||||
base_data = base.data if base else None
|
||||
base_train = base.train if base else None
|
||||
model = ModelConfig(
|
||||
arch=_pick(args.arch, base_model.arch if base_model else None, "cnn"),
|
||||
param_budget=_pick(
|
||||
args.budget, base_model.param_budget if base_model else None, 1_000_000
|
||||
),
|
||||
stride=_pick(args.stride, base_model.stride if base_model else None, 4),
|
||||
d_embed=_pick(args.d_embed, base_model.d_embed if base_model else None, 256),
|
||||
budget_tol=_pick(
|
||||
args.budget_tol, base_model.budget_tol if base_model else None, 0.10
|
||||
),
|
||||
n_heads=_pick(args.n_heads, base_model.n_heads if base_model else None, 4),
|
||||
)
|
||||
data = DataConfig(
|
||||
genera=(
|
||||
_parse_genera(args.genera)
|
||||
if args.genera is not None
|
||||
else (base_data.genera if base_data else ())
|
||||
),
|
||||
val_fraction=_pick(
|
||||
args.val_frac, base_data.val_fraction if base_data else None, 0.10
|
||||
),
|
||||
test_fraction=_pick(
|
||||
args.test_frac, base_data.test_fraction if base_data else None, 0.10
|
||||
),
|
||||
holdout_runs=_pick(
|
||||
args.holdout_runs, base_data.holdout_runs if base_data else None, False
|
||||
),
|
||||
min_test_windows_per_genus=_pick(
|
||||
args.min_test_windows,
|
||||
base_data.min_test_windows_per_genus if base_data else None,
|
||||
300,
|
||||
),
|
||||
seed=_pick(args.data_seed, base_data.seed if base_data else None, 0),
|
||||
)
|
||||
stage = base.stage if base else "arch_ladder"
|
||||
if args.control:
|
||||
stage = "control"
|
||||
elif args.stage:
|
||||
stage = args.stage
|
||||
device = _pick(args.device, base_train.device if base_train else None, None)
|
||||
if device in ("auto", ""):
|
||||
device = None
|
||||
train = TrainConfig(
|
||||
batch_size=_pick(args.batch, base_train.batch_size if base_train else None, 256),
|
||||
lr=_pick(args.lr, base_train.lr if base_train else None, 3e-4),
|
||||
weight_decay=_pick(
|
||||
args.weight_decay, base_train.weight_decay if base_train else None, 0.01
|
||||
),
|
||||
max_epochs=_pick(
|
||||
args.epochs, base_train.max_epochs if base_train else None, 40
|
||||
),
|
||||
warmup_frac=_pick(
|
||||
args.warmup, base_train.warmup_frac if base_train else None, 0.05
|
||||
),
|
||||
label_smoothing=_pick(
|
||||
args.smoothing, base_train.label_smoothing if base_train else None, 0.0
|
||||
),
|
||||
amp=_pick(args.amp, base_train.amp if base_train else None, "bf16"),
|
||||
grad_clip=_pick(
|
||||
args.grad_clip, base_train.grad_clip if base_train else None, 1.0
|
||||
),
|
||||
early_stop_metric=_pick(
|
||||
args.metric, base_train.early_stop_metric if base_train else None, "val_recall_macro"
|
||||
),
|
||||
patience=_pick(args.patience, base_train.patience if base_train else None, 5),
|
||||
num_workers=_pick(
|
||||
args.workers, base_train.num_workers if base_train else None, 4
|
||||
),
|
||||
device=device,
|
||||
seed=_pick(args.seed, base_train.seed if base_train else None, 0),
|
||||
deterministic=_pick(
|
||||
args.deterministic, base_train.deterministic if base_train else None, True
|
||||
),
|
||||
)
|
||||
store_dir = args.store or (base.store_dir if base else None)
|
||||
if store_dir is None:
|
||||
raise ValueError("--store (or a base --config with a store_dir) is required")
|
||||
out_dir = args.out_dir or (base.out_dir if base else None)
|
||||
if out_dir is None:
|
||||
raise ValueError("--out-dir (or a base --config with an out_dir) is required")
|
||||
run_id = args.run_id or (base.run_id if base else "") or ""
|
||||
return RunConfig(
|
||||
run_id=run_id,
|
||||
stage=stage,
|
||||
store_dir=Path(store_dir),
|
||||
data=data,
|
||||
model=model,
|
||||
train=train,
|
||||
out_dir=Path(out_dir),
|
||||
notes=args.notes or (base.notes if base else "") or "",
|
||||
)
|
||||
|
||||
|
||||
def _cmd_build(args: argparse.Namespace) -> int:
|
||||
"""Handle ``build``: resolve a budget and print the realised geometry.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments (arch, budget, stride, d-embed,
|
||||
budget-tol, n-heads, n-classes, seed).
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
"""
|
||||
from .models import build_probe, count_params
|
||||
from .seed import seed_everything
|
||||
|
||||
seed_everything(args.seed)
|
||||
cfg = ModelConfig(
|
||||
arch=args.arch,
|
||||
param_budget=args.budget,
|
||||
stride=args.stride if args.stride is not None else 4,
|
||||
d_embed=args.d_embed if args.d_embed is not None else 256,
|
||||
budget_tol=args.budget_tol if args.budget_tol is not None else 0.10,
|
||||
n_heads=args.n_heads if args.n_heads is not None else 4,
|
||||
)
|
||||
probe, realised = build_probe(cfg, args.n_classes)
|
||||
payload = {
|
||||
"arch": realised.arch,
|
||||
"param_budget": realised.param_budget,
|
||||
"params_realised": realised.params_realised,
|
||||
"stride": realised.stride,
|
||||
"patch_len": min(4, realised.stride),
|
||||
"d_model": realised.d_model,
|
||||
"n_layers": realised.n_layers,
|
||||
"n_heads": realised.n_heads,
|
||||
"d_embed": realised.d_embed,
|
||||
"n_classes": args.n_classes,
|
||||
"count_params": count_params(probe),
|
||||
}
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_store(args: argparse.Namespace) -> int:
|
||||
"""Handle ``store``: build a signal store and print its meta.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments; either ``--synthetic`` or both ``--pod5``
|
||||
and ``--labels`` must be given, plus windowing flags.
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
Raises:
|
||||
ValueError: On an incomplete mode selection (no ``--synthetic``
|
||||
and no pod5/labels pair).
|
||||
|
||||
"""
|
||||
from .store import extract_store, load_labels, synthetic_store
|
||||
|
||||
out = Path(args.out)
|
||||
if args.synthetic:
|
||||
meta = synthetic_store(
|
||||
out_dir=out,
|
||||
n_genera=args.n_genera,
|
||||
reads_per_genus=args.reads_per_genus,
|
||||
window_samples=args.window_samples,
|
||||
period=args.period,
|
||||
noise=args.noise,
|
||||
seed=args.seed,
|
||||
shard_windows=args.shard_windows,
|
||||
genus_prefix=args.genus_prefix,
|
||||
)
|
||||
elif args.pod5 and args.labels:
|
||||
cfg = ExtractConfig(
|
||||
pod5_paths=tuple(Path(p) for p in args.pod5),
|
||||
window_samples=args.window_samples,
|
||||
skip_head_samples=args.skip_head,
|
||||
max_windows_per_read=args.max_windows_per_read,
|
||||
min_read_samples=args.min_read_samples,
|
||||
norm=args.norm,
|
||||
dtype=args.dtype,
|
||||
shard_windows=args.shard_windows,
|
||||
seed=args.seed,
|
||||
)
|
||||
labels = load_labels(Path(args.labels))
|
||||
meta = extract_store(cfg, labels, out)
|
||||
else:
|
||||
raise ValueError(
|
||||
"store needs either --synthetic or both --pod5 PATHS... and --labels PARQUET"
|
||||
)
|
||||
print(json.dumps(meta, indent=2, sort_keys=True, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_train(args: argparse.Namespace) -> int:
|
||||
"""Handle ``train``: run one training run and print its summary.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments (see :func:`_run_config_from_args`).
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
"""
|
||||
cfg = _run_config_from_args(args)
|
||||
realised, result, _ = train_run(cfg)
|
||||
tail = result.history.row(result.history.height - 1, named=True)
|
||||
metric = (
|
||||
tail["val_recall_macro"]
|
||||
if realised.train.early_stop_metric == "val_recall_macro"
|
||||
else tail["val_loss"]
|
||||
)
|
||||
run_dir = realised.out_dir / realised.run_id
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"run_id": realised.run_id,
|
||||
"run_dir": str(run_dir),
|
||||
"arch": realised.model.arch,
|
||||
"d_model": realised.model.d_model,
|
||||
"n_layers": realised.model.n_layers,
|
||||
"params_realised": realised.model.params_realised,
|
||||
"best_epoch": result.best_epoch,
|
||||
f"best_{realised.train.early_stop_metric}": metric,
|
||||
"seconds": round(result.seconds, 1),
|
||||
},
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_validate(args: argparse.Namespace) -> int:
|
||||
"""Handle ``validate``: evaluate a run checkpoint on its val split.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments (``--run-dir``, optional ``--store``,
|
||||
``--n-boot``).
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
"""
|
||||
payload = evaluate_val(
|
||||
Path(args.run_dir), store_dir=Path(args.store) if args.store else None, n_boot=args.n_boot
|
||||
)
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_test(args: argparse.Namespace) -> int:
|
||||
"""Handle ``test``: evaluate a run checkpoint on its test split.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments (``--run-dir``, optional ``--store`` and
|
||||
``--trap-store``, ``--n-boot``).
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
"""
|
||||
payload = evaluate_test(
|
||||
Path(args.run_dir),
|
||||
store_dir=Path(args.store) if args.store else None,
|
||||
trap_store_dir=Path(args.trap_store) if args.trap_store else None,
|
||||
n_boot=args.n_boot,
|
||||
)
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_gate(args: argparse.Namespace) -> int:
|
||||
"""Handle ``gate``: compute and print the E1-v2 gate verdict.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments (``--runs``, ``--control``,
|
||||
``--chance-tol``).
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
"""
|
||||
verdict = evaluate_gate([Path(r) for r in args.runs], Path(args.control), args.chance_tol)
|
||||
print(json.dumps(verdict.__dict__, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_configs(args: argparse.Namespace) -> int:
|
||||
"""Handle ``configs``: load a config JSON and re-save it elsewhere.
|
||||
|
||||
A round-trip helper proving ``load_config(save_config(c)) == c`` and
|
||||
letting users materialise a base config from an existing run.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments (``--config`` source, ``--out``
|
||||
destination).
|
||||
|
||||
Returns:
|
||||
Exit code 0 on success.
|
||||
|
||||
"""
|
||||
cfg = load_config(args.config)
|
||||
save_config(cfg, Path(args.out))
|
||||
print(f"wrote {args.out}")
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""Construct the full CLI parser with all subcommands.
|
||||
|
||||
Returns:
|
||||
The configured ``argparse.ArgumentParser`` (``--help`` lists
|
||||
every subcommand and flag).
|
||||
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="custom_models",
|
||||
description="E1 genus probe: torch models and train/validate/test wrapper",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_build = sub.add_parser("build", help="resolve a param budget into a realised probe")
|
||||
_add_model_args(p_build, required=True)
|
||||
p_build.add_argument("--n-classes", type=int, default=11)
|
||||
p_build.add_argument("--seed", type=int, default=0)
|
||||
p_build.set_defaults(func=_cmd_build)
|
||||
|
||||
p_store = sub.add_parser("store", help="build a signal store (pod5+labels or synthetic)")
|
||||
p_store.add_argument("--out", required=True)
|
||||
p_store.add_argument("--synthetic", action="store_true")
|
||||
p_store.add_argument("--n-genera", type=int, default=11)
|
||||
p_store.add_argument("--reads-per-genus", type=int, default=200)
|
||||
p_store.add_argument("--period", type=int, default=None)
|
||||
p_store.add_argument("--noise", type=float, default=0.15)
|
||||
p_store.add_argument("--genus-prefix", default="genus_")
|
||||
p_store.add_argument("--pod5", nargs="+", default=None)
|
||||
p_store.add_argument("--labels", default=None)
|
||||
p_store.add_argument("--window-samples", type=int, default=12_000)
|
||||
p_store.add_argument("--skip-head", type=int, default=500)
|
||||
p_store.add_argument("--min-read-samples", type=int, default=12_500)
|
||||
p_store.add_argument("--max-windows-per-read", type=int, default=1)
|
||||
p_store.add_argument("--norm", choices=["median_iqr", "median_mad"], default="median_iqr")
|
||||
p_store.add_argument("--dtype", choices=["float16", "float32"], default="float16")
|
||||
p_store.add_argument("--shard-windows", type=int, default=8_192)
|
||||
p_store.add_argument("--seed", type=int, default=0)
|
||||
p_store.set_defaults(func=_cmd_store)
|
||||
|
||||
p_train = sub.add_parser("train", help="train a probe on a signal store")
|
||||
p_train.add_argument("--store", default=None)
|
||||
p_train.add_argument("--out-dir", default=None)
|
||||
p_train.add_argument("--config", default=None)
|
||||
p_train.add_argument("--run-id", default=None)
|
||||
p_train.add_argument(
|
||||
"--stage",
|
||||
choices=["arch_ladder", "windows_ablation", "robustness", "control", "extra"],
|
||||
default=None,
|
||||
)
|
||||
p_train.add_argument("--control", action="store_true", default=None)
|
||||
p_train.add_argument("--notes", default=None)
|
||||
_add_model_args(p_train, required=False)
|
||||
_add_data_args(p_train)
|
||||
_add_train_args(p_train)
|
||||
p_train.set_defaults(func=_cmd_train)
|
||||
|
||||
p_validate = sub.add_parser("validate", help="evaluate a run checkpoint on its val split")
|
||||
p_validate.add_argument("--run-dir", required=True)
|
||||
p_validate.add_argument("--store", default=None)
|
||||
p_validate.add_argument("--n-boot", type=int, default=10_000)
|
||||
p_validate.set_defaults(func=_cmd_validate)
|
||||
|
||||
p_test = sub.add_parser("test", help="evaluate a run checkpoint on its test split")
|
||||
p_test.add_argument("--run-dir", required=True)
|
||||
p_test.add_argument("--store", default=None)
|
||||
p_test.add_argument("--trap-store", default=None)
|
||||
p_test.add_argument("--n-boot", type=int, default=10_000)
|
||||
p_test.set_defaults(func=_cmd_test)
|
||||
|
||||
p_gate = sub.add_parser("gate", help="gate verdict over run dirs against a control run")
|
||||
p_gate.add_argument("--runs", nargs="+", required=True)
|
||||
p_gate.add_argument("--control", required=True)
|
||||
p_gate.add_argument("--chance-tol", type=float, default=1.5)
|
||||
p_gate.set_defaults(func=_cmd_gate)
|
||||
|
||||
p_configs = sub.add_parser("configs", help="round-trip a config JSON (save_config/load_config)")
|
||||
p_configs.add_argument("--config", required=True)
|
||||
p_configs.add_argument("--out", required=True)
|
||||
p_configs.set_defaults(func=_cmd_configs)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
"""CLI entry point (also ``python -m custom_models``).
|
||||
|
||||
Args:
|
||||
argv: Argument list without the program name; ``None`` uses
|
||||
``sys.argv``.
|
||||
|
||||
Returns:
|
||||
0 on success; 1 on usage or runtime errors (message printed to
|
||||
stderr).
|
||||
|
||||
"""
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||
try:
|
||||
return int(args.func(args))
|
||||
except (ValueError, RuntimeError, FileNotFoundError, ImportError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Frozen configuration dataclasses and JSON (de)serialisation.
|
||||
|
||||
The dataclasses mirror the GATE ZERO config contract (``E1_genus_information_
|
||||
ceiling.md`` §5.1) with the E1-v2 deltas (fixed 11-taxa membership, no
|
||||
class-ladder machinery) and the binding deviations recorded in its §12:
|
||||
``ModelConfig`` carries the realised fields ``d_model`` / ``n_layers`` /
|
||||
``params_realised`` that ``build_probe`` fills in, and ``RunConfig`` records
|
||||
the realised genera so a run directory round-trips deterministically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
Arch = Literal["cnn", "linatt"]
|
||||
Stage = Literal["arch_ladder", "windows_ablation", "robustness", "control", "extra"]
|
||||
AmpPolicy = Literal["off", "bf16", "fp16"]
|
||||
EarlyStopMetric = Literal["val_recall_macro", "val_loss"]
|
||||
NormMethod = Literal["median_iqr", "median_mad"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DataConfig:
|
||||
"""Split-time data selection and read-level split parameters."""
|
||||
|
||||
genera: tuple[str, ...] = ()
|
||||
val_fraction: float = 0.10
|
||||
test_fraction: float = 0.10
|
||||
holdout_runs: bool = False
|
||||
min_test_windows_per_genus: int = 300
|
||||
seed: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModelConfig:
|
||||
"""Probe architecture request; realised geometry is filled by ``build_probe``.
|
||||
|
||||
Attributes:
|
||||
arch: Encoder family, ``"cnn"`` or ``"linatt"``.
|
||||
param_budget: Nominal trainable-parameter budget in params (e.g.
|
||||
``1_000_000``). Only the nominal budget enters the run id; the
|
||||
gate arithmetic always uses ``params_realised``.
|
||||
stride: Patch stride in signal samples, 4 or 8 (token pitch).
|
||||
d_embed: Pooled embedding width the encoder terminates in.
|
||||
budget_tol: Relative tolerance around the budget; the realised count
|
||||
must satisfy ``count_params <= (1 + budget_tol) * param_budget``.
|
||||
n_heads: Attention head count for the ``"linatt"`` arch (recorded so
|
||||
checkpoint rehydration rebuilds the exact geometry).
|
||||
d_model: realised token width (filled by ``build_probe``, ``None``
|
||||
on a request config).
|
||||
n_layers: realised block count (filled by ``build_probe``).
|
||||
params_realised: realised trainable parameter count (filled by
|
||||
``build_probe``; the only source of truth for reporting).
|
||||
|
||||
"""
|
||||
|
||||
arch: Arch
|
||||
param_budget: int
|
||||
stride: int
|
||||
d_embed: int = 256
|
||||
budget_tol: float = 0.10
|
||||
n_heads: int = 4
|
||||
d_model: int | None = None
|
||||
n_layers: int | None = None
|
||||
params_realised: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrainConfig:
|
||||
"""Training semantics (GATE ZERO §5.1; restated in the torch model spec §5)."""
|
||||
|
||||
batch_size: int = 256
|
||||
lr: float = 3e-4
|
||||
weight_decay: float = 0.01
|
||||
max_epochs: int = 40
|
||||
warmup_frac: float = 0.05
|
||||
label_smoothing: float = 0.0
|
||||
amp: AmpPolicy = "bf16"
|
||||
grad_clip: float = 1.0
|
||||
early_stop_metric: EarlyStopMetric = "val_recall_macro"
|
||||
patience: int = 5
|
||||
num_workers: int = 4
|
||||
device: str | None = None
|
||||
seed: int = 0
|
||||
deterministic: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExtractConfig:
|
||||
"""Pod5-to-store extraction parameters for ``extract_store``.
|
||||
|
||||
Attributes:
|
||||
pod5_paths: Pod5 files to scan, processed in sorted path order.
|
||||
window_samples: Fixed window length in samples (12,000 ≈ 2.4 s at
|
||||
5 kHz ≈ ~1 kb) — the model-input contract.
|
||||
skip_head_samples: Head samples skipped before the first window
|
||||
(adapter / mux artefacts).
|
||||
max_windows_per_read: Windows taken per read; 1 is the headline, 4 is
|
||||
the Stage-A' ablation row. Windows are contiguous from the first.
|
||||
min_read_samples: Reads shorter than this are dropped and counted in
|
||||
the store meta (must fit skip-head plus one full window).
|
||||
norm: Per-window normaliser applied before storing.
|
||||
dtype: On-disk shard dtype; fp16 halves the store at negligible cost.
|
||||
shard_windows: Windows per ``shard_%05d.npy`` file.
|
||||
seed: Unused by extraction itself (window choice is deterministic);
|
||||
kept so every config in the pipeline is seed-carrying.
|
||||
|
||||
"""
|
||||
|
||||
pod5_paths: tuple[Path, ...]
|
||||
window_samples: int = 12_000
|
||||
skip_head_samples: int = 500
|
||||
max_windows_per_read: int = 1
|
||||
min_read_samples: int = 12_500
|
||||
norm: NormMethod = "median_iqr"
|
||||
dtype: Literal["float16", "float32"] = "float16"
|
||||
shard_windows: int = 8_192
|
||||
seed: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RunConfig:
|
||||
"""One experiment run: a store, data/model/train sub-configs and outputs.
|
||||
|
||||
``train_run`` fills ``run_id`` (if empty) and ``genera`` (from the realised
|
||||
splits) before the realised config is serialised into the run directory.
|
||||
"""
|
||||
|
||||
run_id: str
|
||||
stage: Stage
|
||||
store_dir: Path
|
||||
data: DataConfig
|
||||
model: ModelConfig
|
||||
train: TrainConfig
|
||||
out_dir: Path
|
||||
genera: tuple[str, ...] = ()
|
||||
notes: str = ""
|
||||
|
||||
|
||||
def run_id_for(arch: str, param_budget: int, stride: int, n_genera: int, stage: str) -> str:
|
||||
"""Build the deterministic, filesystem-safe run identifier.
|
||||
|
||||
Format: ``{arch}_{budget in M:.1f}M_s{stride}_g{n_genera}``, e.g.
|
||||
``cnn_1.0M_s4_g11``; the control stage appends ``-shuf``. Only the
|
||||
nominal budget and class count enter the id — realised geometry never
|
||||
does — so ids are unique within the run matrix and stable across
|
||||
rehydration.
|
||||
|
||||
Args:
|
||||
arch: Encoder family name (``"cnn"`` or ``"linatt"``).
|
||||
param_budget: Nominal parameter budget (divided by 1e6 for the tag).
|
||||
stride: Token stride in samples.
|
||||
n_genera: Number of selected classes (the ``g`` component).
|
||||
stage: Run stage; ``"control"`` triggers the ``-shuf`` suffix.
|
||||
|
||||
Returns:
|
||||
The run id string.
|
||||
|
||||
"""
|
||||
run_id = f"{arch}_{param_budget / 1_000_000:.1f}M_s{stride}_g{n_genera}"
|
||||
if stage == "control":
|
||||
return f"{run_id}-shuf"
|
||||
return run_id
|
||||
|
||||
|
||||
_TYPE_KEY = "__type__"
|
||||
_DATACLASSES: dict[str, type] = {
|
||||
cls.__name__: cls
|
||||
for cls in (DataConfig, ModelConfig, TrainConfig, ExtractConfig, RunConfig)
|
||||
}
|
||||
|
||||
|
||||
def _encode(obj: Any) -> Any:
|
||||
"""Recursively convert a config object into a JSON-friendly payload.
|
||||
|
||||
``Path`` becomes ``{"__type__": "path", "value": str}``, tuples become
|
||||
tagged sequences (so they round-trip as tuples), and dataclass instances
|
||||
become dicts tagged with their class name. All other values pass
|
||||
through unchanged.
|
||||
|
||||
Args:
|
||||
obj: A config dataclass, ``Path``, tuple/list, or plain JSON value.
|
||||
|
||||
Returns:
|
||||
The JSON-serialisable encoding of ``obj``.
|
||||
|
||||
"""
|
||||
if isinstance(obj, Path):
|
||||
return {_TYPE_KEY: "path", "value": str(obj)}
|
||||
if isinstance(obj, (tuple, list)):
|
||||
return {_TYPE_KEY: "seq", "value": [_encode(v) for v in obj]}
|
||||
if is_dataclass(obj) and not isinstance(obj, type):
|
||||
payload: dict[str, Any] = {_TYPE_KEY: type(obj).__name__}
|
||||
for field in obj.__dataclass_fields__:
|
||||
payload[field] = _encode(getattr(obj, field))
|
||||
return payload
|
||||
return obj
|
||||
|
||||
|
||||
def _decode(obj: Any) -> Any:
|
||||
"""Invert :func:`_encode` back into config objects.
|
||||
|
||||
Tagged dicts rebuild ``Path`` values, tuples and registered dataclasses;
|
||||
anything else is returned as-is. Unknown ``__type__`` tags fall through
|
||||
as plain dicts so foreign JSON never crashes decoding.
|
||||
|
||||
Args:
|
||||
obj: A payload produced by :func:`_encode` (or nested JSON value).
|
||||
|
||||
Returns:
|
||||
The reconstructed config object.
|
||||
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
kind = obj.get(_TYPE_KEY)
|
||||
if kind == "path":
|
||||
return Path(obj["value"])
|
||||
if kind == "seq":
|
||||
return tuple(_decode(v) for v in obj["value"])
|
||||
if kind in _DATACLASSES:
|
||||
cls = _DATACLASSES[kind]
|
||||
kwargs = {}
|
||||
for name, field in cls.__dataclass_fields__.items():
|
||||
if name in obj:
|
||||
kwargs[name] = _decode(obj[name])
|
||||
elif field.default is not None:
|
||||
pass
|
||||
return cls(**kwargs)
|
||||
return obj
|
||||
|
||||
|
||||
def save_config(cfg: Any, path: Path) -> None:
|
||||
"""Serialise a config dataclass to pretty JSON.
|
||||
|
||||
Parent directories are created as needed. The payload uses the
|
||||
``"__type__"`` discriminator scheme (GATE ZERO §12.7) so
|
||||
``load_config(save_config(c)) == c`` holds exactly.
|
||||
|
||||
Args:
|
||||
cfg: A config dataclass (or nested structure of them).
|
||||
path: Destination JSON file path.
|
||||
|
||||
"""
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(_encode(cfg), indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def load_config(path: Path) -> Any:
|
||||
"""Load a config dataclass written by :func:`save_config`.
|
||||
|
||||
Args:
|
||||
path: JSON config file path.
|
||||
|
||||
Returns:
|
||||
The reconstructed config object (type depends on the file).
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If ``path`` does not exist.
|
||||
ValueError: If the file is not valid JSON or does not decode into
|
||||
the recorded dataclass schema.
|
||||
|
||||
"""
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"config file not found: {path}")
|
||||
try:
|
||||
return _decode(json.loads(path.read_text()))
|
||||
except (KeyError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise ValueError(f"malformed config file: {path}: {exc}") from exc
|
||||
@@ -0,0 +1,406 @@
|
||||
"""Splits, label permutation for control runs, datasets and data loaders.
|
||||
|
||||
Read-level stratified splitting (all windows of a read share a split),
|
||||
the v2 near-clade safety minimum on per-genus test windows, whole-run
|
||||
holdout mode, the seeded shuffled-label permutation used by control runs,
|
||||
and the torch ``Dataset``/``DataLoader`` plumbing with device-appropriate
|
||||
worker/pinning behaviour.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
from .config import DataConfig, TrainConfig
|
||||
from .seed import make_generator, worker_init_fn
|
||||
from .store import SignalStore
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Splits:
|
||||
"""Window-index splits plus the full-store label vector.
|
||||
|
||||
Attributes:
|
||||
train: Sorted window ids assigned to training.
|
||||
val: Sorted window ids assigned to validation.
|
||||
test: Sorted window ids assigned to testing.
|
||||
genera: Sorted selected genera; class id = position in this list
|
||||
everywhere downstream.
|
||||
labels: int32 class id for every store window (length = store
|
||||
size), ``-1`` for windows outside the selected genera.
|
||||
|
||||
"""
|
||||
|
||||
train: np.ndarray
|
||||
val: np.ndarray
|
||||
test: np.ndarray
|
||||
genera: list[str]
|
||||
labels: np.ndarray
|
||||
|
||||
|
||||
def make_splits(manifest: pl.DataFrame, cfg: DataConfig) -> Splits:
|
||||
"""Compute deterministic read-level stratified splits from a manifest.
|
||||
|
||||
Selection: ``cfg.genera`` if given (sorted, must all exist in the
|
||||
store — the fixed owner list in E1 v2), else every genus present.
|
||||
Splitting: per genus, reads are shuffled with the seeded generator and
|
||||
assigned by read counts (test first, then val, remainder train), so
|
||||
all windows of a read land in one split. ``holdout_runs`` instead
|
||||
assigns whole ``source_run`` groups to test (seeded shuffle until the
|
||||
test fraction is met) and splits the remaining reads into
|
||||
train/val, requiring every genus to retain both train and test
|
||||
presence. The v2 near-clade guard enforces
|
||||
``min_test_windows_per_genus`` test windows per genus.
|
||||
|
||||
Args:
|
||||
manifest: Store manifest (one row per window; needs ``window_id``,
|
||||
``read_id``, ``genus``, ``source_run``).
|
||||
cfg: Split parameters (genera, fractions, holdout mode, minimum,
|
||||
seed).
|
||||
|
||||
Returns:
|
||||
The splits (see :class:`Splits`).
|
||||
|
||||
Raises:
|
||||
ValueError: On missing manifest columns, an empty manifest,
|
||||
requested genera absent from the store, an empty selection or
|
||||
train split, holdout presence failure, or a per-genus test
|
||||
count below ``min_test_windows_per_genus`` (v2: raise instead
|
||||
of gating on a noisy plan).
|
||||
|
||||
"""
|
||||
for column in ("window_id", "read_id", "genus", "source_run"):
|
||||
if column not in manifest.columns:
|
||||
raise ValueError(f"manifest missing column: {column}")
|
||||
n = manifest.height
|
||||
if n == 0:
|
||||
raise ValueError("manifest is empty")
|
||||
genus_arr = manifest["genus"].to_numpy()
|
||||
read_arr = manifest["read_id"].to_numpy()
|
||||
wid_arr = manifest["window_id"].to_numpy()
|
||||
run_arr = manifest["source_run"].to_numpy()
|
||||
all_genera = sorted(set(genus_arr.tolist()))
|
||||
selected = sorted(set(cfg.genera)) if cfg.genera else list(all_genera)
|
||||
if not selected:
|
||||
raise ValueError("no genera available for splitting")
|
||||
missing = [g for g in selected if g not in set(all_genera)]
|
||||
if missing:
|
||||
raise ValueError(f"genera absent from the store: {missing}")
|
||||
lookup = {g: i for i, g in enumerate(selected)}
|
||||
n_classes = len(selected)
|
||||
labels = np.full(n, -1, dtype=np.int32)
|
||||
for genus, class_id in lookup.items():
|
||||
labels[genus_arr == genus] = class_id
|
||||
selected_mask = labels >= 0
|
||||
if not selected_mask.any():
|
||||
raise ValueError("no windows belong to the selected genera")
|
||||
|
||||
rng = make_generator(cfg.seed)
|
||||
windows_by_read: dict[str, list[int]] = {}
|
||||
genus_by_read: dict[str, str] = {}
|
||||
run_by_read: dict[str, str] = {}
|
||||
for wid, read_id, genus, source_run in zip(
|
||||
wid_arr[selected_mask], read_arr[selected_mask], genus_arr[selected_mask],
|
||||
run_arr[selected_mask]
|
||||
):
|
||||
windows_by_read.setdefault(read_id, []).append(int(wid))
|
||||
genus_by_read[read_id] = genus
|
||||
run_by_read[read_id] = source_run
|
||||
|
||||
reads_by_genus: dict[str, list[str]] = {g: [] for g in selected}
|
||||
for read_id, genus in genus_by_read.items():
|
||||
reads_by_genus[genus].append(read_id)
|
||||
for reads in reads_by_genus.values():
|
||||
reads.sort()
|
||||
|
||||
train_ids: list[int] = []
|
||||
val_ids: list[int] = []
|
||||
test_ids: list[int] = []
|
||||
|
||||
if cfg.holdout_runs:
|
||||
runs = sorted(set(run_by_read.values()))
|
||||
windows_per_run: dict[str, int] = {}
|
||||
for read_id, source_run in run_by_read.items():
|
||||
windows_per_run[source_run] = windows_per_run.get(source_run, 0) + len(
|
||||
windows_by_read[read_id]
|
||||
)
|
||||
order = rng.permutation(len(runs))
|
||||
shuffled_runs = [runs[i] for i in order]
|
||||
target_test = round(cfg.test_fraction * selected_mask.sum())
|
||||
chosen: set[str] = set()
|
||||
accrued = 0
|
||||
for source_run in shuffled_runs:
|
||||
if accrued >= target_test:
|
||||
break
|
||||
chosen.add(source_run)
|
||||
accrued += windows_per_run[source_run]
|
||||
for read_id, source_run in run_by_read.items():
|
||||
if source_run in chosen:
|
||||
test_ids.extend(windows_by_read[read_id])
|
||||
for genus in selected:
|
||||
reads = [r for r in reads_by_genus[genus] if run_by_read[r] not in chosen]
|
||||
order = rng.permutation(len(reads))
|
||||
shuffled = [reads[i] for i in order]
|
||||
n_val = round(cfg.val_fraction * len(shuffled))
|
||||
val_reads = shuffled[:n_val]
|
||||
train_reads = shuffled[n_val:]
|
||||
for r in val_reads:
|
||||
val_ids.extend(windows_by_read[r])
|
||||
for r in train_reads:
|
||||
train_ids.extend(windows_by_read[r])
|
||||
for genus in selected:
|
||||
has_train = any(run_by_read[r] not in chosen for r in reads_by_genus[genus])
|
||||
has_test = any(run_by_read[r] in chosen for r in reads_by_genus[genus])
|
||||
if not (has_train and has_test):
|
||||
raise ValueError(
|
||||
f"holdout_runs split leaves genus {genus!r} without train and test presence"
|
||||
)
|
||||
else:
|
||||
for genus in selected:
|
||||
reads = reads_by_genus[genus]
|
||||
order = rng.permutation(len(reads))
|
||||
shuffled = [reads[i] for i in order]
|
||||
n_test = round(cfg.test_fraction * len(shuffled))
|
||||
n_val = round(cfg.val_fraction * len(shuffled))
|
||||
test_reads = shuffled[:n_test]
|
||||
val_reads = shuffled[n_test : n_test + n_val]
|
||||
train_reads = shuffled[n_test + n_val :]
|
||||
for r in test_reads:
|
||||
test_ids.extend(windows_by_read[r])
|
||||
for r in val_reads:
|
||||
val_ids.extend(windows_by_read[r])
|
||||
for r in train_reads:
|
||||
train_ids.extend(windows_by_read[r])
|
||||
|
||||
if not train_ids:
|
||||
raise ValueError("train split is empty after splitting")
|
||||
train = np.array(sorted(train_ids), dtype=np.int64)
|
||||
val = np.array(sorted(val_ids), dtype=np.int64)
|
||||
test = np.array(sorted(test_ids), dtype=np.int64)
|
||||
if cfg.min_test_windows_per_genus > 0:
|
||||
if len(test) == 0:
|
||||
raise ValueError(
|
||||
"test split is empty; per-genus minimum "
|
||||
f"min_test_windows_per_genus={cfg.min_test_windows_per_genus} cannot be met"
|
||||
)
|
||||
counts = np.bincount(labels[test], minlength=n_classes)
|
||||
short = {
|
||||
selected[i]: int(c)
|
||||
for i, c in enumerate(counts)
|
||||
if 0 < c < cfg.min_test_windows_per_genus
|
||||
}
|
||||
absent = [g for g in selected if not np.any(labels[test] == lookup[g])]
|
||||
if short or absent:
|
||||
raise ValueError(
|
||||
"per-genus test windows below the near-clade safety minimum "
|
||||
f"(min_test_windows_per_genus={cfg.min_test_windows_per_genus}): "
|
||||
f"short={short} absent={absent}"
|
||||
)
|
||||
return Splits(train=train, val=val, test=test, genera=selected, labels=labels)
|
||||
|
||||
|
||||
def permute_read_labels(
|
||||
manifest: pl.DataFrame, genera: tuple[str, ...], seed: int
|
||||
) -> pl.DataFrame:
|
||||
"""Shuffle genus labels across reads for the Stage-C control run.
|
||||
|
||||
The read-to-genus mapping of the selected pool is permuted (seeded,
|
||||
preserving the genus multiset exactly); every window of a read
|
||||
inherits its read's new label. Applied *before* splitting so the
|
||||
control run's splits and labels live in the same permuted world as
|
||||
its training (evaluation re-permutes identically).
|
||||
|
||||
Args:
|
||||
manifest: Store manifest to relabel.
|
||||
genera: Restriction of the permutation pool; empty means all
|
||||
genera present.
|
||||
seed: Seed for the permutation.
|
||||
|
||||
Returns:
|
||||
A new manifest with the ``genus`` column permuted.
|
||||
|
||||
Raises:
|
||||
ValueError: If the pool holds fewer than two reads.
|
||||
|
||||
"""
|
||||
genus_arr = manifest["genus"].to_numpy()
|
||||
read_arr = manifest["read_id"].to_numpy()
|
||||
genus_by_read: dict[str, str] = {}
|
||||
for read_id, genus in zip(read_arr, genus_arr):
|
||||
genus_by_read.setdefault(read_id, genus)
|
||||
pool = sorted(genus_by_read)
|
||||
if genera:
|
||||
pool = [r for r in pool if genus_by_read[r] in set(genera)]
|
||||
if len(pool) < 2:
|
||||
raise ValueError("control run needs at least two reads in the selected pool")
|
||||
values = [genus_by_read[r] for r in pool]
|
||||
rng = make_generator(seed)
|
||||
order = rng.permutation(len(values))
|
||||
permuted = {read_id: values[i] for read_id, i in zip(pool, order)}
|
||||
new_genus = [permuted[read_id] for read_id in read_arr]
|
||||
return manifest.with_columns(pl.Series("genus", new_genus, dtype=pl.String))
|
||||
|
||||
|
||||
class ProbeDataset(torch.utils.data.Dataset):
|
||||
"""Window dataset over a subset of store window ids."""
|
||||
|
||||
def __init__(self, store: SignalStore, indices: np.ndarray, labels: np.ndarray) -> None:
|
||||
"""Bind a store, window ids and the full-store label vector.
|
||||
|
||||
Args:
|
||||
store: Open signal store serving the windows.
|
||||
indices: Window ids to serve (a split, or all ids).
|
||||
labels: Full-store label vector indexed by window id
|
||||
(from :class:`Splits`).
|
||||
|
||||
"""
|
||||
self.store = store
|
||||
self.indices = np.asarray(indices, dtype=np.int64)
|
||||
self.labels = np.asarray(labels, dtype=np.int64)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of windows in this dataset."""
|
||||
return len(self.indices)
|
||||
|
||||
def __getitem__(self, index: int) -> tuple[np.ndarray, int]:
|
||||
"""Fetch one window and its class id.
|
||||
|
||||
Args:
|
||||
index: Dataset row index (not the window id).
|
||||
|
||||
Returns:
|
||||
``(float32 window of shape (L,), class id)``.
|
||||
|
||||
"""
|
||||
window_id = int(self.indices[index])
|
||||
return self.store.get(window_id), int(self.labels[window_id])
|
||||
|
||||
|
||||
def collate_windows(
|
||||
batch: list[tuple[np.ndarray, int]],
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Stack dataset samples into model-ready tensors.
|
||||
|
||||
Args:
|
||||
batch: List of ``(window, class_id)`` pairs from
|
||||
:class:`ProbeDataset`.
|
||||
|
||||
Returns:
|
||||
``(windows (B, L) float32, labels (B,) int64)`` — the probe's
|
||||
input contract.
|
||||
|
||||
"""
|
||||
windows = torch.from_numpy(np.stack([np.asarray(b[0], dtype=np.float32) for b in batch]))
|
||||
labels = torch.tensor([int(b[1]) for b in batch], dtype=torch.int64)
|
||||
return windows, labels
|
||||
|
||||
|
||||
def _loader_mp_context():
|
||||
"""Pick a DataLoader multiprocessing start context for this platform.
|
||||
|
||||
Linux pins ``"fork"`` (torch's historical default) because conda's
|
||||
Python 3.14 builds ship a broken ``forkserver``; other platforms keep
|
||||
the interpreter default (spawn on macOS/Windows).
|
||||
|
||||
Returns:
|
||||
A start-method name or ``None`` for the platform default.
|
||||
|
||||
"""
|
||||
if sys.platform == "linux":
|
||||
return "fork"
|
||||
return None
|
||||
|
||||
|
||||
def make_loaders(
|
||||
store: SignalStore, splits: Splits, cfg: TrainConfig
|
||||
) -> tuple[DataLoader, DataLoader, DataLoader]:
|
||||
"""Build the train/val/test loaders for one run.
|
||||
|
||||
Train: shuffled over a generator seeded from ``cfg.seed`` with
|
||||
``drop_last=True`` (stable step counts) and per-worker seeding.
|
||||
Val/test: unshuffled and complete. ``pin_memory`` only when the
|
||||
resolved device is CUDA; ``persistent_workers`` only when
|
||||
``num_workers > 0``.
|
||||
|
||||
Args:
|
||||
store: The signal store both splits read from.
|
||||
splits: Split window ids and labels.
|
||||
cfg: Training config (batch size, workers, seed, device).
|
||||
|
||||
Returns:
|
||||
``(train_loader, val_loader, test_loader)``.
|
||||
|
||||
"""
|
||||
from .train import select_device
|
||||
|
||||
device = select_device(cfg.device)
|
||||
pin = device.type == "cuda"
|
||||
generator = torch.Generator()
|
||||
generator.manual_seed(cfg.seed)
|
||||
common: dict = {
|
||||
"batch_size": cfg.batch_size,
|
||||
"collate_fn": collate_windows,
|
||||
"num_workers": cfg.num_workers,
|
||||
"pin_memory": pin,
|
||||
"persistent_workers": cfg.num_workers > 0,
|
||||
"worker_init_fn": worker_init_fn,
|
||||
"multiprocessing_context": _loader_mp_context() if cfg.num_workers > 0 else None,
|
||||
}
|
||||
train_loader = DataLoader(
|
||||
ProbeDataset(store, splits.train, splits.labels),
|
||||
shuffle=True,
|
||||
generator=generator,
|
||||
drop_last=True,
|
||||
**common,
|
||||
)
|
||||
val_loader = DataLoader(
|
||||
ProbeDataset(store, splits.val, splits.labels),
|
||||
shuffle=False,
|
||||
drop_last=False,
|
||||
**common,
|
||||
)
|
||||
test_loader = DataLoader(
|
||||
ProbeDataset(store, splits.test, splits.labels),
|
||||
shuffle=False,
|
||||
drop_last=False,
|
||||
**common,
|
||||
)
|
||||
return train_loader, val_loader, test_loader
|
||||
|
||||
|
||||
def make_full_loader(store: SignalStore, cfg: TrainConfig) -> DataLoader:
|
||||
"""Build an unshuffled loader over *every* window of a store.
|
||||
|
||||
Used for trap (TP2) evaluation where the store's genera are not
|
||||
classes of the probe; labels are placeholder ``-1`` values.
|
||||
|
||||
Args:
|
||||
store: Store to evaluate end-to-end.
|
||||
cfg: Training config supplying batching/worker/device behaviour.
|
||||
|
||||
Returns:
|
||||
A DataLoader yielding ``(windows, -1 labels)`` in window order.
|
||||
|
||||
"""
|
||||
from .train import select_device
|
||||
|
||||
device = select_device(cfg.device)
|
||||
labels = np.full(len(store), -1, dtype=np.int64)
|
||||
return DataLoader(
|
||||
ProbeDataset(store, np.arange(len(store), dtype=np.int64), labels),
|
||||
batch_size=cfg.batch_size,
|
||||
shuffle=False,
|
||||
drop_last=False,
|
||||
collate_fn=collate_windows,
|
||||
num_workers=cfg.num_workers,
|
||||
pin_memory=device.type == "cuda",
|
||||
persistent_workers=cfg.num_workers > 0,
|
||||
worker_init_fn=worker_init_fn,
|
||||
multiprocessing_context=_loader_mp_context() if cfg.num_workers > 0 else None,
|
||||
)
|
||||
@@ -0,0 +1,494 @@
|
||||
"""Hand-rolled metrics on CPU numpy (no scikit-learn, no device-dependent math).
|
||||
|
||||
Closed-set reporting with seeded bootstrap CIs, the max-softmax-probability
|
||||
(MSP) open-set diagnostic with val-calibrated tau, and the E1-v2 trap
|
||||
report (false-accept rates at tau, family-collapsed impostor rates, MSP /
|
||||
margin histograms, top-3 retrieval and embedding centroids for the E3
|
||||
hook). Conventions: per-genus recall is NaN for genera absent from the
|
||||
evaluated split; precision and F1 use the zero-division=0 convention so
|
||||
reports stay JSON-clean.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
|
||||
DEFAULT_TRAP_ADJACENCY: dict[str, set[str]] = {
|
||||
"Paenibacillus": {"Bacillus"},
|
||||
"Escherichia": {
|
||||
"Cronobacter",
|
||||
"Citrobacter",
|
||||
"Enterobacter",
|
||||
"Klebsiella",
|
||||
"Salmonella",
|
||||
"Shigella",
|
||||
},
|
||||
"Aeromonas": {"Vibrio"},
|
||||
}
|
||||
|
||||
|
||||
def _softmax(logits: np.ndarray) -> np.ndarray:
|
||||
"""Row-wise softmax, numerically stable (max-shifted).
|
||||
|
||||
Args:
|
||||
logits: ``(N, C)`` score matrix.
|
||||
|
||||
Returns:
|
||||
``(N, C)`` probability matrix (rows sum to 1).
|
||||
|
||||
"""
|
||||
z = logits - logits.max(axis=1, keepdims=True)
|
||||
e = np.exp(z)
|
||||
return e / e.sum(axis=1, keepdims=True)
|
||||
|
||||
|
||||
def msp(logits: np.ndarray) -> np.ndarray:
|
||||
"""Max-softmax-probability per row — the open-set confidence score.
|
||||
|
||||
Args:
|
||||
logits: Logit matrix of shape ``(N, C)`` with ``C >= 2``.
|
||||
|
||||
Returns:
|
||||
``(N,)`` float64 MSP values in ``(0, 1]``.
|
||||
|
||||
Raises:
|
||||
ValueError: On non-2-D input or fewer than two classes.
|
||||
|
||||
"""
|
||||
logits = np.asarray(logits, dtype=np.float64)
|
||||
if logits.ndim != 2 or logits.shape[1] < 2:
|
||||
raise ValueError("msp expects logits of shape (N, C) with C >= 2")
|
||||
return _softmax(logits).max(axis=1)
|
||||
|
||||
|
||||
def _confusion(y_true: np.ndarray, y_pred: np.ndarray, n_classes: int) -> np.ndarray:
|
||||
"""Build a ``(C, C)`` count matrix (rows = true, cols = predicted).
|
||||
|
||||
Args:
|
||||
y_true: ``(N,)`` int true class ids.
|
||||
y_pred: ``(N,)`` int predicted class ids.
|
||||
n_classes: Class count C (matrix is always C x C even for absent
|
||||
classes).
|
||||
|
||||
Returns:
|
||||
int64 confusion-count matrix.
|
||||
|
||||
"""
|
||||
codes = y_true.astype(np.int64) * n_classes + y_pred.astype(np.int64)
|
||||
return np.bincount(codes, minlength=n_classes * n_classes).reshape(n_classes, n_classes)
|
||||
|
||||
|
||||
def macro_recall(y_true: np.ndarray, y_pred: np.ndarray, n_classes: int) -> float:
|
||||
"""Macro genus recall — the headline gate metric.
|
||||
|
||||
Mean over genera of ``correct / windows of that genus``; genera absent
|
||||
from ``y_true`` are skipped (NaN-safe). This is the cheap per-epoch
|
||||
variant used during training.
|
||||
|
||||
Args:
|
||||
y_true: ``(N,)`` int true class ids.
|
||||
y_pred: ``(N,)`` int predicted class ids.
|
||||
n_classes: Class count C.
|
||||
|
||||
Returns:
|
||||
Macro recall in ``[0, 1]``; NaN for an empty input pair.
|
||||
|
||||
"""
|
||||
y_true = np.asarray(y_true, dtype=np.int64)
|
||||
y_pred = np.asarray(y_pred, dtype=np.int64)
|
||||
if y_true.size == 0:
|
||||
return float("nan")
|
||||
cm = _confusion(y_true, y_pred, n_classes)
|
||||
rows = cm.sum(axis=1)
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
recall = np.where(rows > 0, np.diag(cm) / rows, np.nan)
|
||||
return float(np.nanmean(recall))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClosedSetReport:
|
||||
"""Closed-set evaluation summary.
|
||||
|
||||
Attributes:
|
||||
recall_macro: Mean per-genus recall (headline gate metric).
|
||||
recall_micro: Top-1 accuracy (== micro recall).
|
||||
precision_macro: Mean per-class precision (zero-division = 0).
|
||||
f1_macro: Mean per-class F1 (zero-division = 0).
|
||||
per_genus: DataFrame of genus, n_test, recall, precision, f1 and a
|
||||
``trap`` boolean (False for closed-set rows; v2 schema).
|
||||
confusion: ``(C, C)`` count matrix, rows = true, cols = pred,
|
||||
ordered like ``genera``.
|
||||
ci: ``"recall_macro"`` / ``"recall_micro"`` -> 95% bootstrap CI
|
||||
(empty when ``n_boot == 0``).
|
||||
|
||||
"""
|
||||
|
||||
recall_macro: float
|
||||
recall_micro: float
|
||||
precision_macro: float
|
||||
f1_macro: float
|
||||
per_genus: pl.DataFrame
|
||||
confusion: np.ndarray
|
||||
ci: dict[str, tuple[float, float]]
|
||||
|
||||
|
||||
def closed_set_report(
|
||||
y_true: np.ndarray,
|
||||
y_pred: np.ndarray,
|
||||
genera: list[str],
|
||||
n_boot: int = 10_000,
|
||||
seed: int = 0,
|
||||
) -> ClosedSetReport:
|
||||
"""Full closed-set report over predicted labels, with bootstrap CIs.
|
||||
|
||||
The bootstrap resamples evaluation windows with replacement
|
||||
``n_boot`` times (seeded) and recomputes macro/micro recall; the 2.5 /
|
||||
97.5 percentiles give the 95% CIs. E1 fixes ``n_boot=10_000`` and
|
||||
``seed=0`` for cross-run comparability.
|
||||
|
||||
Args:
|
||||
y_true: ``(N,)`` int true class ids in ``[0, C)``.
|
||||
y_pred: ``(N,)`` int predicted class ids in ``[0, C)``.
|
||||
genera: Class names; position = class id (the split's genera
|
||||
list).
|
||||
n_boot: Bootstrap resample count (0 disables CIs).
|
||||
seed: Seed for the bootstrap generator.
|
||||
|
||||
Returns:
|
||||
The :class:`ClosedSetReport`.
|
||||
|
||||
Raises:
|
||||
ValueError: On length mismatch, fewer than 2 classes, an empty
|
||||
evaluation set, or labels outside ``[0, C)``.
|
||||
|
||||
"""
|
||||
y_true = np.asarray(y_true, dtype=np.int64)
|
||||
y_pred = np.asarray(y_pred, dtype=np.int64)
|
||||
if len(y_true) != len(y_pred):
|
||||
raise ValueError("y_true and y_pred must have equal length")
|
||||
n_classes = len(genera)
|
||||
if n_classes < 2:
|
||||
raise ValueError("closed-set report needs at least 2 classes")
|
||||
if y_true.size == 0:
|
||||
raise ValueError("cannot report on an empty evaluation set")
|
||||
lo_ok = y_true.min() >= 0 and y_pred.min() >= 0
|
||||
hi_ok = y_true.max() < n_classes and y_pred.max() < n_classes
|
||||
if not (lo_ok and hi_ok):
|
||||
raise ValueError("labels fall outside [0, n_classes)")
|
||||
confusion = _confusion(y_true, y_pred, n_classes)
|
||||
row_sums = confusion.sum(axis=1)
|
||||
col_sums = confusion.sum(axis=0)
|
||||
diag = np.diag(confusion)
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
recall = np.where(row_sums > 0, diag / row_sums, np.nan)
|
||||
precision = np.where(col_sums > 0, diag / col_sums, 0.0)
|
||||
f1 = np.zeros(n_classes, dtype=np.float64)
|
||||
valid = (row_sums > 0) & (np.nan_to_num(recall) + precision > 0)
|
||||
f1[valid] = (
|
||||
2 * recall[valid] * precision[valid] / (recall[valid] + precision[valid])
|
||||
)
|
||||
recall_macro = float(np.nanmean(recall)) if not np.isnan(recall).all() else 0.0
|
||||
recall_micro = float(diag.sum() / confusion.sum())
|
||||
precision_macro = float(precision.mean())
|
||||
f1_macro = float(f1.mean())
|
||||
per_genus = pl.DataFrame(
|
||||
{
|
||||
"genus": genera,
|
||||
"n_test": row_sums.astype(np.int64),
|
||||
"recall": recall,
|
||||
"precision": precision,
|
||||
"f1": f1,
|
||||
"trap": np.zeros(n_classes, dtype=bool),
|
||||
}
|
||||
)
|
||||
ci: dict[str, tuple[float, float]] = {}
|
||||
if n_boot > 0:
|
||||
rng = np.random.default_rng(seed)
|
||||
n = len(y_true)
|
||||
pair_codes = y_true * n_classes + y_pred
|
||||
macro_samples = np.empty(n_boot, dtype=np.float64)
|
||||
micro_samples = np.empty(n_boot, dtype=np.float64)
|
||||
for b in range(n_boot):
|
||||
idx = rng.integers(0, n, size=n)
|
||||
cm = np.bincount(pair_codes[idx], minlength=n_classes * n_classes).reshape(
|
||||
n_classes, n_classes
|
||||
)
|
||||
rows = cm.sum(axis=1)
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
rec = np.where(rows > 0, np.diag(cm) / rows, np.nan)
|
||||
macro_samples[b] = np.nanmean(rec)
|
||||
micro_samples[b] = np.diag(cm).sum() / cm.sum()
|
||||
ci["recall_macro"] = (
|
||||
float(np.quantile(macro_samples, 0.025)),
|
||||
float(np.quantile(macro_samples, 0.975)),
|
||||
)
|
||||
ci["recall_micro"] = (
|
||||
float(np.quantile(micro_samples, 0.025)),
|
||||
float(np.quantile(micro_samples, 0.975)),
|
||||
)
|
||||
return ClosedSetReport(
|
||||
recall_macro=recall_macro,
|
||||
recall_micro=recall_micro,
|
||||
precision_macro=precision_macro,
|
||||
f1_macro=f1_macro,
|
||||
per_genus=per_genus,
|
||||
confusion=confusion,
|
||||
ci=ci,
|
||||
)
|
||||
|
||||
|
||||
def choose_tau_msp(
|
||||
val_correct_msp: np.ndarray, target_known_recall: float = 0.95
|
||||
) -> float:
|
||||
"""Calibrate the open-set MSP threshold tau on correct validation reads.
|
||||
|
||||
``tau`` is the ``(1 - target)`` quantile of the MSP values of
|
||||
*correctly classified* validation windows, so at least ``target`` of
|
||||
correct known reads survive ``msp >= tau``.
|
||||
|
||||
Args:
|
||||
val_correct_msp: MSP values of correct validation windows.
|
||||
target_known_recall: Fraction of correct known reads to keep
|
||||
(default 0.95, GATE ZERO §5.9).
|
||||
|
||||
Returns:
|
||||
The threshold ``tau`` in ``[0, 1)``.
|
||||
|
||||
Raises:
|
||||
ValueError: On empty input or a target outside ``(0, 1)``.
|
||||
|
||||
"""
|
||||
arr = np.asarray(val_correct_msp, dtype=np.float64)
|
||||
if arr.size == 0:
|
||||
raise ValueError("choose_tau_msp needs at least one correctly classified window")
|
||||
if not 0.0 < target_known_recall < 1.0:
|
||||
raise ValueError("target_known_recall must lie in (0, 1)")
|
||||
return float(np.quantile(arr, 1.0 - target_known_recall))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpenSetProbeReport:
|
||||
"""E1's simple open-set diagnostic (full calibration is E3's job).
|
||||
|
||||
Attributes:
|
||||
tau: Val-calibrated MSP threshold.
|
||||
false_unknown_rate: Fraction of ALL test windows with
|
||||
``msp < tau`` (routed to "unknown").
|
||||
known_recall_at_tau: Macro recall restricted to windows with
|
||||
``msp >= tau``.
|
||||
kept_fraction: Fraction of test windows with ``msp >= tau``.
|
||||
|
||||
"""
|
||||
|
||||
tau: float
|
||||
false_unknown_rate: float
|
||||
known_recall_at_tau: float
|
||||
kept_fraction: float
|
||||
|
||||
|
||||
def open_set_probe_report(
|
||||
val_logits: np.ndarray,
|
||||
val_y: np.ndarray,
|
||||
test_logits: np.ndarray,
|
||||
test_y: np.ndarray,
|
||||
target_known_recall: float = 0.95,
|
||||
) -> OpenSetProbeReport:
|
||||
"""Compute the open-set probe report from val and test logits.
|
||||
|
||||
Tau comes from the correct-validation MSP quantile
|
||||
(:func:`choose_tau_msp`); the rates are measured on the test split.
|
||||
|
||||
Args:
|
||||
val_logits: ``(N_val, C)`` validation logits.
|
||||
val_y: ``(N_val,)`` validation class ids.
|
||||
test_logits: ``(N_test, C)`` test logits.
|
||||
test_y: ``(N_test,)`` test class ids.
|
||||
target_known_recall: Known-recall target for tau calibration.
|
||||
|
||||
Returns:
|
||||
The :class:`OpenSetProbeReport`.
|
||||
|
||||
Raises:
|
||||
ValueError: On empty validation/test inputs or a validation pass
|
||||
with zero correct windows (tau undefined).
|
||||
|
||||
"""
|
||||
val_logits = np.asarray(val_logits, dtype=np.float64)
|
||||
val_y = np.asarray(val_y, dtype=np.int64)
|
||||
if val_logits.size == 0:
|
||||
raise ValueError("open-set probe needs a non-empty validation set")
|
||||
val_pred = val_logits.argmax(axis=1)
|
||||
correct = val_pred == val_y
|
||||
if not correct.any():
|
||||
raise ValueError("open-set probe needs at least one correct validation window")
|
||||
tau = choose_tau_msp(msp(val_logits)[correct], target_known_recall)
|
||||
test_logits = np.asarray(test_logits, dtype=np.float64)
|
||||
test_y = np.asarray(test_y, dtype=np.int64)
|
||||
if test_logits.size == 0:
|
||||
raise ValueError("open-set probe needs a non-empty test set")
|
||||
test_msp = msp(test_logits)
|
||||
test_pred = test_logits.argmax(axis=1)
|
||||
kept = test_msp >= tau
|
||||
kept_fraction = float(kept.mean())
|
||||
false_unknown_rate = float((~kept).mean())
|
||||
n_classes = test_logits.shape[1]
|
||||
known_recall_at_tau = (
|
||||
macro_recall(test_y[kept], test_pred[kept], n_classes) if kept.any() else float("nan")
|
||||
)
|
||||
return OpenSetProbeReport(
|
||||
tau=tau,
|
||||
false_unknown_rate=false_unknown_rate,
|
||||
known_recall_at_tau=known_recall_at_tau,
|
||||
kept_fraction=kept_fraction,
|
||||
)
|
||||
|
||||
|
||||
def _quantiles(values: np.ndarray) -> dict[str, float]:
|
||||
"""Summarise a value vector with the 5/25/50/75/95 percentiles.
|
||||
|
||||
Args:
|
||||
values: 1-D array (assumed within a known range).
|
||||
|
||||
Returns:
|
||||
Dict with keys ``p05`` .. ``p95``.
|
||||
|
||||
"""
|
||||
q = np.quantile(values, [0.05, 0.25, 0.5, 0.75, 0.95])
|
||||
return {
|
||||
"p05": float(q[0]),
|
||||
"p25": float(q[1]),
|
||||
"p50": float(q[2]),
|
||||
"p75": float(q[3]),
|
||||
"p95": float(q[4]),
|
||||
}
|
||||
|
||||
|
||||
def _histogram(values: np.ndarray, bins: int = 20) -> dict[str, Any]:
|
||||
"""Histogram of a value vector over the unit interval.
|
||||
|
||||
Args:
|
||||
values: 1-D array in ``[0, 1]`` (MSP values, margins).
|
||||
bins: Bin count.
|
||||
|
||||
Returns:
|
||||
``{"edges": [...], "counts": [...]}`` (JSON-friendly).
|
||||
|
||||
"""
|
||||
counts, edges = np.histogram(values, bins=bins, range=(0.0, 1.0))
|
||||
return {"edges": [float(e) for e in edges], "counts": [int(c) for c in counts]}
|
||||
|
||||
|
||||
def trap_report(
|
||||
trap_manifest: pl.DataFrame,
|
||||
trap_logits: np.ndarray,
|
||||
tau: float,
|
||||
genera: list[str],
|
||||
adjacency: dict[str, set[str]] | None = None,
|
||||
embeddings: np.ndarray | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the TP2 trap report for eval-only impostor windows.
|
||||
|
||||
Trap genera are never trained classes; every window is pushed through
|
||||
the frozen probe's 11-way head and scored post-hoc. Per trap genus
|
||||
this reports: window/read counts and provenance runs, the false-accept
|
||||
rate at the val-calibrated ``tau`` (``fpr_at_tau``), the
|
||||
family-collapsed impostor rate (``fpr_family`` — prediction in the
|
||||
biologically adjacent classes), MSP and top1-top2 margin quantiles and
|
||||
unit histograms, per-rank top-3 predicted-class counts, and the mean
|
||||
embedding centroid (the E3 hook). Nothing is calibrated *on* traps —
|
||||
tau always comes from the main run's validation split.
|
||||
|
||||
Args:
|
||||
trap_manifest: The trap store's manifest (row order = logits
|
||||
order; genus column names the trap genus per window).
|
||||
trap_logits: ``(N, C)`` logits from the frozen probe.
|
||||
tau: Open-set threshold from :func:`open_set_probe_report`.
|
||||
genera: The probe's class list (position = class id).
|
||||
adjacency: Map trap genus -> adjacent class-genera set; defaults
|
||||
to the owner-list mapping from the v2 design (Paenibacillus ->
|
||||
Bacillus, Escherichia -> the six Enterobacteriaceae classes,
|
||||
Aeromonas -> Vibrio). Unknown trap genera simply get an empty
|
||||
family set.
|
||||
embeddings: Optional ``(N, d_embed)`` pooled embeddings (from
|
||||
:func:`~custom_models.train.embed_probe`) enabling centroid
|
||||
output.
|
||||
|
||||
Returns:
|
||||
JSON-serialisable dict: overall ``trap_fpr_at_tau`` /
|
||||
``trap_fpr_family`` plus a ``per_trap`` breakdown.
|
||||
|
||||
Raises:
|
||||
ValueError: If manifest rows and logits disagree.
|
||||
|
||||
"""
|
||||
adjacency = adjacency if adjacency is not None else DEFAULT_TRAP_ADJACENCY
|
||||
trap_logits = np.asarray(trap_logits, dtype=np.float64)
|
||||
n = trap_logits.shape[0]
|
||||
if len(trap_manifest) != n:
|
||||
raise ValueError("trap manifest rows and logits disagree")
|
||||
probs = _softmax(trap_logits)
|
||||
trap_msp = probs.max(axis=1)
|
||||
trap_pred = trap_logits.argmax(axis=1)
|
||||
order = np.argsort(-probs, axis=1)
|
||||
top1 = order[:, 0]
|
||||
top2 = order[:, 1]
|
||||
margins = probs[np.arange(n), top1] - probs[np.arange(n), top2]
|
||||
genus_arr = trap_manifest["genus"].to_numpy()
|
||||
run_arr = trap_manifest["source_run"].to_numpy()
|
||||
read_arr = trap_manifest["read_id"].to_numpy()
|
||||
accepted = trap_msp >= tau
|
||||
family_ids_by_genus: dict[str, list[int]] = {}
|
||||
for trap_genus in set(genus_arr.tolist()):
|
||||
family = adjacency.get(trap_genus, set())
|
||||
family_ids_by_genus[trap_genus] = [
|
||||
genera.index(g) for g in family if g in genera
|
||||
]
|
||||
in_family = np.array(
|
||||
[
|
||||
trap_pred[i] in family_ids_by_genus.get(genus_arr[i], [])
|
||||
for i in range(n)
|
||||
],
|
||||
dtype=bool,
|
||||
)
|
||||
per_trap: dict[str, Any] = {}
|
||||
for trap_genus in sorted(set(genus_arr.tolist())):
|
||||
idx = np.flatnonzero(genus_arr == trap_genus)
|
||||
family_ids = family_ids_by_genus[trap_genus]
|
||||
pred_g = trap_pred[idx]
|
||||
fpr_family = float(np.isin(pred_g, sorted(family_ids)).mean()) if len(idx) else 0.0
|
||||
ranks = {"rank1": {}, "rank2": {}, "rank3": {}}
|
||||
for rank, col in ((0, "rank1"), (1, "rank2"), (2, "rank3")):
|
||||
counts: dict[str, int] = {}
|
||||
for c in order[idx, rank]:
|
||||
name = genera[int(c)]
|
||||
counts[name] = counts.get(name, 0) + 1
|
||||
ranks[col] = dict(sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])))
|
||||
entry: dict[str, Any] = {
|
||||
"n_eval_windows": len(idx),
|
||||
"source_runs": sorted(set(run_arr[idx].tolist())),
|
||||
"n_reads": len(set(read_arr[idx].tolist())),
|
||||
"fpr_at_tau": float(accepted[idx].mean()),
|
||||
"fpr_family": fpr_family,
|
||||
"adjacent_classes": sorted(family),
|
||||
"msp_quantiles": _quantiles(trap_msp[idx]),
|
||||
"margin_quantiles": _quantiles(margins[idx]),
|
||||
"msp_hist": _histogram(trap_msp[idx]),
|
||||
"margin_hist": _histogram(margins[idx]),
|
||||
"top3_by_rank": ranks,
|
||||
}
|
||||
if embeddings is not None and len(idx):
|
||||
entry["centroid"] = [float(v) for v in embeddings[idx].mean(axis=0)]
|
||||
else:
|
||||
entry["centroid"] = None
|
||||
per_trap[trap_genus] = entry
|
||||
return {
|
||||
"tau": float(tau),
|
||||
"n_eval_windows": int(n),
|
||||
"trap_fpr_at_tau": float(accepted.mean()) if n else 0.0,
|
||||
"trap_fpr_family": float(in_family.mean()) if n else 0.0,
|
||||
"per_trap": per_trap,
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
"""Torch modules for the E1 supervised genus probe.
|
||||
|
||||
Implements the normative model spec (``E1_torch_model_spec.md`` §3): a
|
||||
``SignalPatchEmbed`` conv patchifier (no positional information), the
|
||||
``ConvEncoder`` (depthwise-separable blocks, dilation capped at 16,
|
||||
zero-initialised attention-weighted pooling) and the
|
||||
``LinearAttentionEncoder`` (pre-norm "fast transformer" blocks with the
|
||||
elu+1 kernel, plain mean pooling), both terminating in a pooled
|
||||
``d_embed`` vector, plus the linear-headed ``GenusProbe`` and the
|
||||
deterministic budget resolver ``build_probe``. No batch-norm anywhere
|
||||
(MPS/AMP-hostile, device-identical semantics required); no softmax or
|
||||
log-softmax inside the model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from .config import ModelConfig
|
||||
|
||||
D_MODEL_LATTICE: tuple[int, ...] = (
|
||||
32,
|
||||
48,
|
||||
64,
|
||||
96,
|
||||
128,
|
||||
192,
|
||||
256,
|
||||
384,
|
||||
512,
|
||||
768,
|
||||
1024,
|
||||
1536,
|
||||
2048,
|
||||
3072,
|
||||
4096,
|
||||
)
|
||||
N_LAYERS_LATTICE: tuple[int, ...] = (2, 3, 4, 6, 8, 12, 16)
|
||||
DILATION_CAP = 16
|
||||
LINEAR_ATTENTION_EPS = 1e-6
|
||||
|
||||
|
||||
def count_params(module: nn.Module) -> int:
|
||||
"""Count trainable parameters of a module (buffers excluded).
|
||||
|
||||
This is the single source of truth for budget checks and reporting —
|
||||
closed-form parameter arithmetic is design intuition only (spec §4).
|
||||
|
||||
Args:
|
||||
module: Any ``torch.nn.Module`` (typically a ``GenusProbe``).
|
||||
|
||||
Returns:
|
||||
Total ``numel`` over parameters with ``requires_grad=True``.
|
||||
|
||||
"""
|
||||
return sum(p.numel() for p in module.parameters() if p.requires_grad)
|
||||
|
||||
|
||||
class SignalPatchEmbed(nn.Module):
|
||||
"""Patchify raw signal into tokens with a strided 1-D convolution.
|
||||
|
||||
``Conv1d(1 -> d_model, kernel=patch_len, stride=stride)`` followed by a
|
||||
transpose to token layout ``(B, T, d_model)``. Bias is kept (it is part
|
||||
of the parameter count). No positional information is added: absolute
|
||||
position within a read is biologically irrelevant for a genus signal,
|
||||
and ~3k-token positional tables would eat a large fraction of the 1M
|
||||
budget point (spec §3.1).
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, patch_len: int, stride: int) -> None:
|
||||
"""Create the patch embedding.
|
||||
|
||||
Args:
|
||||
d_model: Token width produced by the convolution.
|
||||
patch_len: Kernel size in signal samples (E1 uses
|
||||
``min(4, stride)``).
|
||||
stride: Token pitch in signal samples (4 or 8).
|
||||
|
||||
Raises:
|
||||
ValueError: If any argument is < 1.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
if d_model < 1:
|
||||
raise ValueError("d_model must be >= 1")
|
||||
if patch_len < 1:
|
||||
raise ValueError("patch_len must be >= 1")
|
||||
if stride < 1:
|
||||
raise ValueError("stride must be >= 1")
|
||||
self.patch_len = patch_len
|
||||
self.stride = stride
|
||||
self.proj = nn.Conv1d(1, d_model, kernel_size=patch_len, stride=stride)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Map a raw-signal batch to tokens.
|
||||
|
||||
Args:
|
||||
x: Signal batch of shape ``(B, L)`` float32; ``L`` must be
|
||||
divisible by ``stride`` (guaranteed by the store, validated
|
||||
defensively per the API contract).
|
||||
|
||||
Returns:
|
||||
Tokens of shape ``(B, T, d_model)`` with
|
||||
``T = (L - patch_len) // stride + 1`` (3,000 for L=12,000 and
|
||||
stride 4).
|
||||
|
||||
Raises:
|
||||
ValueError: If ``x`` is not 2-D or ``L % stride != 0``.
|
||||
|
||||
"""
|
||||
if x.dim() != 2:
|
||||
raise ValueError(f"expected signal batch of shape (B, L), got {tuple(x.shape)}")
|
||||
if x.shape[1] % self.stride != 0:
|
||||
raise ValueError(
|
||||
f"window length {x.shape[1]} is not divisible by stride {self.stride}"
|
||||
)
|
||||
tokens = self.proj(x.unsqueeze(1))
|
||||
return tokens.transpose(1, 2)
|
||||
|
||||
|
||||
class ConvBlock(nn.Module):
|
||||
"""One depthwise-separable conv block: depthwise -> pointwise -> LN -> GELU.
|
||||
|
||||
Padding ``dilation * (k - 1) // 2`` preserves the token count so
|
||||
``T`` is invariant through the stack. Post-norm ordering follows the
|
||||
GATE ZERO block sketch (spec §3.2).
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, kernel_size: int, dilation: int, dropout: float) -> None:
|
||||
"""Create the block.
|
||||
|
||||
Args:
|
||||
d_model: Token width (channels in/Out).
|
||||
kernel_size: Depthwise kernel size (7 anchors layer 0's
|
||||
receptive field, 5 afterwards).
|
||||
dilation: Depthwise dilation (growth capped at
|
||||
``DILATION_CAP`` by the caller).
|
||||
dropout: Dropout probability applied after activation; 0.0
|
||||
installs an identity (E1 matrices run at 0.0).
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
padding = dilation * (kernel_size - 1) // 2
|
||||
self.depthwise = nn.Conv1d(
|
||||
d_model,
|
||||
d_model,
|
||||
kernel_size=kernel_size,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
groups=d_model,
|
||||
)
|
||||
self.pointwise = nn.Conv1d(d_model, d_model, kernel_size=1)
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
self.act = nn.GELU()
|
||||
self.drop = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Run the block over a token tensor.
|
||||
|
||||
Args:
|
||||
x: Tokens of shape ``(B, T, d_model)``.
|
||||
|
||||
Returns:
|
||||
Tokens of the same shape (length-preserving padding).
|
||||
|
||||
"""
|
||||
h = x.transpose(1, 2)
|
||||
h = self.pointwise(self.depthwise(h))
|
||||
h = h.transpose(1, 2)
|
||||
return self.drop(self.act(self.norm(h)))
|
||||
|
||||
|
||||
class ConvEncoder(nn.Module):
|
||||
"""Depthwise-separable 1-D CNN encoder with attention-weighted pooling.
|
||||
|
||||
Stack of ``n_layers`` :class:`ConvBlock` instances — kernel 7 in layer
|
||||
0 (receptive-field anchor) then 5, dilation ``min(2**l, 16)`` (GATE
|
||||
ZERO §12.3 cap) — followed by ``weighted_mean_pool``: a zero-init
|
||||
``Linear(d_model -> 1)`` score head (uniform pooling at init, plain
|
||||
mean behaviour) softmaxed over tokens, applied to a
|
||||
``Linear(d_model -> d_embed)`` projection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
n_layers: int,
|
||||
patch_len: int,
|
||||
stride: int,
|
||||
d_embed: int,
|
||||
dropout: float = 0.0,
|
||||
) -> None:
|
||||
"""Create the encoder.
|
||||
|
||||
Args:
|
||||
d_model: Token width inside the stack.
|
||||
n_layers: Number of conv blocks.
|
||||
patch_len: Patch length passed to the patch embed.
|
||||
stride: Token pitch passed to the patch embed.
|
||||
d_embed: Output pooled-embedding width.
|
||||
dropout: Block dropout (E1 uses 0.0; do not tune inside E1 —
|
||||
that changes the ladder semantics).
|
||||
|
||||
Raises:
|
||||
ValueError: If ``n_layers`` or ``d_embed`` < 1 (patch-embed
|
||||
args are validated in :class:`SignalPatchEmbed`).
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
if n_layers < 1:
|
||||
raise ValueError("n_layers must be >= 1")
|
||||
if d_embed < 1:
|
||||
raise ValueError("d_embed must be >= 1")
|
||||
self.embed = SignalPatchEmbed(d_model, patch_len, stride)
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
ConvBlock(
|
||||
d_model,
|
||||
kernel_size=7 if layer == 0 else 5,
|
||||
dilation=min(2**layer, DILATION_CAP),
|
||||
dropout=dropout,
|
||||
)
|
||||
for layer in range(n_layers)
|
||||
]
|
||||
)
|
||||
self.score = nn.Linear(d_model, 1)
|
||||
nn.init.zeros_(self.score.weight)
|
||||
nn.init.zeros_(self.score.bias)
|
||||
self.proj = nn.Linear(d_model, d_embed)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Encode raw signal to a pooled embedding.
|
||||
|
||||
Args:
|
||||
x: Signal batch of shape ``(B, L)`` float32 with ``L``
|
||||
divisible by ``stride``.
|
||||
|
||||
Returns:
|
||||
Pooled embedding of shape ``(B, d_embed)``.
|
||||
|
||||
"""
|
||||
h = self.embed(x)
|
||||
for block in self.blocks:
|
||||
h = block(h)
|
||||
weights = torch.softmax(self.score(h), dim=1)
|
||||
return (weights * self.proj(h)).sum(dim=1)
|
||||
|
||||
|
||||
class LinearAttention(nn.Module):
|
||||
"""Non-causal linear attention with the elu+1 feature map.
|
||||
|
||||
Computes ``out[t] = phi(q_t) @ (sum_s phi(k_s) v_s^T) / (phi(q_t) @
|
||||
sum_s phi(k_s))`` per head via the associative form — ``O(T*d^2)``
|
||||
compute and ``O(d^2)`` memory per layer, no ``T x T`` matrix — which
|
||||
is what keeps ~3k-token sequences trainable inside the 1-3 GPU-h/run
|
||||
budget. ``phi(x) = elu(x) + 1`` is strictly positive, so the
|
||||
denominator is positive; a small floor guards degenerate cases.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, n_heads: int) -> None:
|
||||
"""Create the attention mixer.
|
||||
|
||||
Args:
|
||||
d_model: Token width (queries/keys/values are full-width
|
||||
linears reshaped to heads, so param count is head-count
|
||||
invariant).
|
||||
n_heads: Number of heads (default 4; recorded in the realised
|
||||
``ModelConfig``).
|
||||
|
||||
Raises:
|
||||
ValueError: If ``d_model`` is not divisible by ``n_heads`` or
|
||||
``n_heads`` < 1.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
if n_heads < 1:
|
||||
raise ValueError("n_heads must be >= 1")
|
||||
if d_model % n_heads != 0:
|
||||
raise ValueError(f"d_model {d_model} not divisible by n_heads {n_heads}")
|
||||
self.n_heads = n_heads
|
||||
self.head_dim = d_model // n_heads
|
||||
self.q = nn.Linear(d_model, d_model)
|
||||
self.k = nn.Linear(d_model, d_model)
|
||||
self.v = nn.Linear(d_model, d_model)
|
||||
self.out = nn.Linear(d_model, d_model)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Mix tokens with linear attention.
|
||||
|
||||
Args:
|
||||
x: Tokens of shape ``(B, T, d_model)``.
|
||||
|
||||
Returns:
|
||||
Mixed tokens of the same shape.
|
||||
|
||||
"""
|
||||
b, t, d = x.shape
|
||||
q = self.q(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2)
|
||||
k = self.k(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2)
|
||||
v = self.v(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2)
|
||||
phi_q = F.elu(q) + 1.0
|
||||
phi_k = F.elu(k) + 1.0
|
||||
kv = torch.einsum("bhti,bhtj->bhij", phi_k, v)
|
||||
k_sum = phi_k.sum(dim=2)
|
||||
numerator = torch.einsum("bhti,bhij->bhtj", phi_q, kv)
|
||||
denominator = torch.einsum("bhti,bhi->bht", phi_q, k_sum)
|
||||
out = numerator / denominator.clamp_min(LINEAR_ATTENTION_EPS).unsqueeze(-1)
|
||||
out = out.transpose(1, 2).reshape(b, t, d)
|
||||
return self.out(out)
|
||||
|
||||
|
||||
class LinearAttentionBlock(nn.Module):
|
||||
"""Pre-norm transformer block: attention residual, then MLP residual."""
|
||||
|
||||
def __init__(self, d_model: int, n_heads: int, dropout: float) -> None:
|
||||
"""Create the block.
|
||||
|
||||
Args:
|
||||
d_model: Token width.
|
||||
n_heads: Attention head count.
|
||||
dropout: Residual dropout (0.0 in E1 matrices).
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
self.norm1 = nn.LayerNorm(d_model)
|
||||
self.attn = LinearAttention(d_model, n_heads)
|
||||
self.norm2 = nn.LayerNorm(d_model)
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(d_model, d_model),
|
||||
nn.GELU(),
|
||||
nn.Linear(d_model, d_model),
|
||||
)
|
||||
self.drop1 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
self.drop2 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Run both mixer stages with pre-norm residuals.
|
||||
|
||||
Args:
|
||||
x: Tokens of shape ``(B, T, d_model)``.
|
||||
|
||||
Returns:
|
||||
Tokens of the same shape.
|
||||
|
||||
"""
|
||||
x = x + self.drop1(self.attn(self.norm1(x)))
|
||||
x = x + self.drop2(self.mlp(self.norm2(x)))
|
||||
return x
|
||||
|
||||
|
||||
class LinearAttentionEncoder(nn.Module):
|
||||
"""Patch embed + pre-norm linear-attention blocks + mean pooling.
|
||||
|
||||
Terminates in the same interchangeable pooled representation as
|
||||
:class:`ConvEncoder`: plain mean over tokens followed by a
|
||||
``Linear(d_model -> d_embed)`` projection (no score head — the pooling
|
||||
is deliberately not attention-weighted here, spec §3.3).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
n_layers: int,
|
||||
patch_len: int,
|
||||
stride: int,
|
||||
d_embed: int,
|
||||
dropout: float = 0.0,
|
||||
n_heads: int = 4,
|
||||
) -> None:
|
||||
"""Create the encoder.
|
||||
|
||||
Args:
|
||||
d_model: Token width inside the stack.
|
||||
n_layers: Number of attention blocks.
|
||||
patch_len: Patch length for the (separately weighted) patch
|
||||
embed.
|
||||
stride: Token pitch for the patch embed.
|
||||
d_embed: Output pooled-embedding width.
|
||||
dropout: Residual dropout (0.0 in E1 matrices).
|
||||
n_heads: Attention head count.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``n_layers`` or ``d_embed`` < 1; head
|
||||
divisibility is enforced in :class:`LinearAttention`.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
if n_layers < 1:
|
||||
raise ValueError("n_layers must be >= 1")
|
||||
if d_embed < 1:
|
||||
raise ValueError("d_embed must be >= 1")
|
||||
self.embed = SignalPatchEmbed(d_model, patch_len, stride)
|
||||
self.blocks = nn.ModuleList(
|
||||
[LinearAttentionBlock(d_model, n_heads, dropout) for _ in range(n_layers)]
|
||||
)
|
||||
self.proj = nn.Linear(d_model, d_embed)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Encode raw signal to a pooled embedding.
|
||||
|
||||
Args:
|
||||
x: Signal batch of shape ``(B, L)`` float32 with ``L``
|
||||
divisible by ``stride``.
|
||||
|
||||
Returns:
|
||||
Pooled embedding of shape ``(B, d_embed)``.
|
||||
|
||||
"""
|
||||
h = self.embed(x)
|
||||
for block in self.blocks:
|
||||
h = block(h)
|
||||
return self.proj(h.mean(dim=1))
|
||||
|
||||
|
||||
class GenusProbe(nn.Module):
|
||||
"""The supervised probe: an encoder plus a bare linear classifier.
|
||||
|
||||
The head is exactly ``Linear(d_embed, n_classes)`` — no hidden MLP, no
|
||||
temperature, no bias tricks — because E1 measures the *encoder's*
|
||||
information content; a linear head on a pooled representation is the
|
||||
cleanest statement of "the information is (or is not) in the
|
||||
embedding". Raw logits only; cross-entropy supplies the softmax.
|
||||
"""
|
||||
|
||||
def __init__(self, encoder: nn.Module, d_embed: int, n_classes: int) -> None:
|
||||
"""Assemble the probe.
|
||||
|
||||
Args:
|
||||
encoder: An encoder (``ConvEncoder`` or
|
||||
``LinearAttentionEncoder``) mapping ``(B, L)`` to
|
||||
``(B, d_embed)``.
|
||||
d_embed: Width of the encoder's pooled output; must match or
|
||||
the first forward raises on the head's shape mismatch.
|
||||
n_classes: Number of genera (C = 11 for E1 v2).
|
||||
|
||||
Raises:
|
||||
ValueError: If ``n_classes`` < 2 (empty classification head) or
|
||||
``d_embed`` < 1.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
if n_classes < 2:
|
||||
raise ValueError("n_classes must be >= 2")
|
||||
if d_embed < 1:
|
||||
raise ValueError("d_embed must be >= 1")
|
||||
self.encoder = encoder
|
||||
self.head = nn.Linear(d_embed, n_classes)
|
||||
|
||||
def embed(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Return the pooled embedding (E3/E4 reuse path).
|
||||
|
||||
Args:
|
||||
x: Signal batch of shape ``(B, L)`` float32.
|
||||
|
||||
Returns:
|
||||
Embeddings of shape ``(B, d_embed)`` — not L2-normalised;
|
||||
cosine machinery belongs to E3, not the probe.
|
||||
|
||||
"""
|
||||
return self.encoder(x)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Classify raw signal.
|
||||
|
||||
Args:
|
||||
x: Signal batch of shape ``(B, L)`` float32.
|
||||
|
||||
Returns:
|
||||
Logits of shape ``(B, n_classes)``.
|
||||
|
||||
"""
|
||||
return self.head(self.embed(x))
|
||||
|
||||
|
||||
def _make_encoder(
|
||||
arch: str,
|
||||
d_model: int,
|
||||
n_layers: int,
|
||||
patch_len: int,
|
||||
stride: int,
|
||||
d_embed: int,
|
||||
n_heads: int,
|
||||
) -> nn.Module:
|
||||
"""Instantiate an encoder of the requested family.
|
||||
|
||||
Args:
|
||||
arch: ``"cnn"`` or ``"linatt"``.
|
||||
d_model: Token width.
|
||||
n_layers: Block count.
|
||||
patch_len: Patch length in samples.
|
||||
stride: Token pitch in samples.
|
||||
d_embed: Pooled output width.
|
||||
n_heads: Head count (ignored by ``"cnn"``).
|
||||
|
||||
Returns:
|
||||
A fresh encoder module (default torch init; draws from the global
|
||||
RNG so callers must seed first).
|
||||
|
||||
Raises:
|
||||
ValueError: On an unknown arch.
|
||||
|
||||
"""
|
||||
if arch == "cnn":
|
||||
return ConvEncoder(d_model, n_layers, patch_len, stride, d_embed)
|
||||
if arch == "linatt":
|
||||
return LinearAttentionEncoder(
|
||||
d_model, n_layers, patch_len, stride, d_embed, n_heads=n_heads
|
||||
)
|
||||
raise ValueError(f"unknown arch: {arch!r}")
|
||||
|
||||
|
||||
def build_probe(cfg: ModelConfig, n_classes: int) -> tuple[GenusProbe, ModelConfig]:
|
||||
"""Resolve a parameter budget into a concrete probe (deterministic).
|
||||
|
||||
Searches the ``(d_model, n_layers)`` lattice — ``D_MODEL_LATTICE`` x
|
||||
``N_LAYERS_LATTICE``, pruning by monotonicity (parameter counts grow
|
||||
with both axes) so oversized candidates are never constructed — and
|
||||
among candidates with ``count_params <= (1 + budget_tol) * budget``
|
||||
picks the *largest* (ceiling-cap reading of GATE ZERO §12.3; candidates
|
||||
above the cap are disqualified even if closer in absolute distance).
|
||||
Ties break by ``(n_layers asc, d_model asc)``. The returned realised
|
||||
config is the value serialised into ``config.json``; re-running the
|
||||
search on it reproduces the same geometry (checkpoint rehydration
|
||||
path). Construction draws only from torch's global RNG: callers must
|
||||
call ``seed_everything`` first, and identical (cfg, n_classes, seed)
|
||||
yields bit-identical weights.
|
||||
|
||||
Args:
|
||||
cfg: Model request (arch, nominal budget, stride, d_embed,
|
||||
tolerance, head count); realised fields are overwritten.
|
||||
n_classes: Number of genera (C = 11 for E1 v2).
|
||||
|
||||
Returns:
|
||||
``(probe, realised_cfg)`` where ``realised_cfg`` fills ``d_model``,
|
||||
``n_layers`` and ``params_realised``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``n_classes`` < 2, ``stride``/``d_embed`` invalid,
|
||||
or no lattice candidate lands within tolerance (the ladder
|
||||
matrix then drops that arch/budget cell — fail fast).
|
||||
|
||||
"""
|
||||
if n_classes < 2:
|
||||
raise ValueError(f"n_classes must be >= 2, got {n_classes}")
|
||||
if cfg.stride < 1:
|
||||
raise ValueError("stride must be >= 1")
|
||||
if cfg.d_embed < 1:
|
||||
raise ValueError("d_embed must be >= 1")
|
||||
patch_len = min(4, cfg.stride)
|
||||
ceiling = int((1.0 + cfg.budget_tol) * cfg.param_budget)
|
||||
best: tuple[int, int, int, GenusProbe] | None = None
|
||||
for d_model in D_MODEL_LATTICE:
|
||||
probe = GenusProbe(
|
||||
_make_encoder(
|
||||
cfg.arch,
|
||||
d_model,
|
||||
N_LAYERS_LATTICE[0],
|
||||
patch_len,
|
||||
cfg.stride,
|
||||
cfg.d_embed,
|
||||
cfg.n_heads,
|
||||
),
|
||||
cfg.d_embed,
|
||||
n_classes,
|
||||
)
|
||||
params = count_params(probe)
|
||||
if params > ceiling:
|
||||
break
|
||||
local: tuple[int, int, int, GenusProbe] = (params, N_LAYERS_LATTICE[0], d_model, probe)
|
||||
for n_layers in N_LAYERS_LATTICE[1:]:
|
||||
probe = GenusProbe(
|
||||
_make_encoder(
|
||||
cfg.arch, d_model, n_layers, patch_len, cfg.stride, cfg.d_embed, cfg.n_heads
|
||||
),
|
||||
cfg.d_embed,
|
||||
n_classes,
|
||||
)
|
||||
params = count_params(probe)
|
||||
if params > ceiling:
|
||||
break
|
||||
local = (params, n_layers, d_model, probe)
|
||||
if best is None or (local[0], -local[1], -local[2]) > (best[0], -best[1], -best[2]):
|
||||
best = local
|
||||
if best is None:
|
||||
raise ValueError(
|
||||
f"no ({cfg.arch}) candidate within {cfg.budget_tol:.0%} of budget {cfg.param_budget}"
|
||||
)
|
||||
params, n_layers, d_model, probe = best
|
||||
realised = replace(
|
||||
cfg,
|
||||
d_model=d_model,
|
||||
n_layers=n_layers,
|
||||
params_realised=params,
|
||||
n_heads=cfg.n_heads,
|
||||
)
|
||||
return probe, realised
|
||||
@@ -0,0 +1,611 @@
|
||||
"""Run orchestration: artifact writing, checkpoint rehydration and the gate.
|
||||
|
||||
``train_run`` executes one run end to end (seed, split, build, train,
|
||||
artifacts); ``evaluate_val`` / ``evaluate_test`` rehydrate a run directory
|
||||
deterministically (same config + same store -> same splits, control runs
|
||||
re-permute identically) and write the evaluation artifact set; and
|
||||
``evaluate_gate`` implements the E1-v2 four-line verdict table over run
|
||||
directories plus the shuffled-label control.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import asdict, dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
import torch
|
||||
|
||||
from .config import RunConfig, load_config, run_id_for, save_config
|
||||
from .data import (
|
||||
Splits,
|
||||
make_full_loader,
|
||||
make_loaders,
|
||||
make_splits,
|
||||
permute_read_labels,
|
||||
)
|
||||
from .metrics import (
|
||||
ClosedSetReport,
|
||||
closed_set_report,
|
||||
msp,
|
||||
open_set_probe_report,
|
||||
trap_report,
|
||||
)
|
||||
from .models import build_probe
|
||||
from .store import SignalStore
|
||||
from .train import (
|
||||
TrainResult,
|
||||
embed_probe,
|
||||
predict,
|
||||
resolve_amp,
|
||||
select_device,
|
||||
train_probe,
|
||||
)
|
||||
|
||||
CONFIG_NAME = "config.json"
|
||||
CKPT_NAME = "ckpt.pt"
|
||||
HISTORY_NAME = "history.parquet"
|
||||
REPORT_NAME = "report.json"
|
||||
VAL_REPORT_NAME = "val_report.json"
|
||||
PER_GENUS_NAME = "per_genus.parquet"
|
||||
CONFUSION_NAME = "confusion.npz"
|
||||
OPEN_SET_NAME = "open_set.json"
|
||||
ERRORS_NAME = "errors.parquet"
|
||||
TRAP_REPORT_NAME = "trap_report.json"
|
||||
GATE_NAME = "gate_verdict.json"
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: Any) -> None:
|
||||
"""Write a JSON artifact atomically enough for run directories.
|
||||
|
||||
Args:
|
||||
path: Destination file (parents created as needed).
|
||||
payload: JSON-serialisable object (dicts/dataclasses already
|
||||
converted by callers).
|
||||
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def train_run(cfg: RunConfig) -> tuple[RunConfig, TrainResult, Splits]:
|
||||
"""Execute one training run and write its core artifacts.
|
||||
|
||||
Order of operations (all stochastic steps seeded from the config):
|
||||
``seed_everything`` -> open store -> permute labels if the stage is
|
||||
``"control"`` -> deterministic splits -> ``build_probe`` budget
|
||||
resolution (run id defaults to ``run_id_for`` when unset) -> write the
|
||||
realised ``config.json`` -> ``train_probe`` (writes
|
||||
``history.parquet``) -> save ``ckpt.pt`` holding the *best* state.
|
||||
Re-runs of the same config overwrite artifacts (idempotent).
|
||||
|
||||
Args:
|
||||
cfg: The run configuration. ``cfg.run_id`` may be empty (then
|
||||
derived from arch/budget/stride/class count/stage).
|
||||
|
||||
Returns:
|
||||
``(realised_cfg, result, splits)`` — the realised config that was
|
||||
serialised, the training outcome and the splits used.
|
||||
|
||||
Raises:
|
||||
Whatever ``make_splits``, ``build_probe`` and ``train_probe``
|
||||
raise on invalid data or configuration (ValueError /
|
||||
RuntimeError / FileNotFoundError).
|
||||
|
||||
"""
|
||||
from .seed import seed_everything
|
||||
|
||||
seed_everything(cfg.train.seed, cfg.train.deterministic)
|
||||
store = SignalStore(cfg.store_dir)
|
||||
manifest = store.manifest()
|
||||
if cfg.stage == "control":
|
||||
manifest = permute_read_labels(manifest, cfg.data.genera, cfg.data.seed)
|
||||
splits = make_splits(manifest, cfg.data)
|
||||
n_classes = len(splits.genera)
|
||||
probe, realised_model = build_probe(cfg.model, n_classes)
|
||||
run_id = cfg.run_id
|
||||
if not run_id:
|
||||
run_id = run_id_for(
|
||||
cfg.model.arch, cfg.model.param_budget, cfg.model.stride, n_classes, cfg.stage
|
||||
)
|
||||
realised = replace(
|
||||
cfg,
|
||||
run_id=run_id,
|
||||
model=realised_model,
|
||||
genera=tuple(splits.genera),
|
||||
)
|
||||
run_dir = realised.out_dir / run_id
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
save_config(realised, run_dir / CONFIG_NAME)
|
||||
result = train_probe(probe, store, splits, realised.train, run_dir)
|
||||
torch.save(
|
||||
{
|
||||
"best_state": result.best_state,
|
||||
"best_epoch": result.best_epoch,
|
||||
"run_id": run_id,
|
||||
"params_realised": realised_model.params_realised,
|
||||
},
|
||||
run_dir / CKPT_NAME,
|
||||
)
|
||||
return realised, result, splits
|
||||
|
||||
|
||||
def load_run(run_dir: Path) -> RunConfig:
|
||||
"""Load a run directory's realised ``RunConfig``.
|
||||
|
||||
Args:
|
||||
run_dir: Directory holding ``config.json`` (as written by
|
||||
:func:`train_run`).
|
||||
|
||||
Returns:
|
||||
The realised run configuration.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the config file is missing.
|
||||
ValueError: If the file does not decode into a ``RunConfig``.
|
||||
|
||||
"""
|
||||
cfg = load_config(Path(run_dir) / CONFIG_NAME)
|
||||
if not isinstance(cfg, RunConfig):
|
||||
raise ValueError(f"{Path(run_dir) / CONFIG_NAME} does not hold a RunConfig")
|
||||
return cfg
|
||||
|
||||
|
||||
def load_probe(run_dir: Path, cfg: RunConfig) -> torch.nn.Module:
|
||||
"""Rebuild a run's probe and load its best checkpoint weights.
|
||||
|
||||
The geometry comes from re-running the deterministic
|
||||
``build_probe`` search on the realised config (a pure function of
|
||||
config + class count, so the shapes always match), then
|
||||
``load_state_dict`` fills the weights.
|
||||
|
||||
Args:
|
||||
run_dir: Run directory holding ``ckpt.pt``.
|
||||
cfg: The run's realised config (model section + genera).
|
||||
|
||||
Returns:
|
||||
The probe (on CPU) with best-epoch weights loaded.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the checkpoint is missing.
|
||||
|
||||
"""
|
||||
run_dir = Path(run_dir)
|
||||
ckpt_path = run_dir / CKPT_NAME
|
||||
if not ckpt_path.is_file():
|
||||
raise FileNotFoundError(f"checkpoint not found: {ckpt_path}")
|
||||
n_classes = len(cfg.genera)
|
||||
probe, _ = build_probe(cfg.model, n_classes)
|
||||
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
||||
probe.load_state_dict(ckpt["best_state"])
|
||||
return probe
|
||||
|
||||
|
||||
def _rehydrate(
|
||||
run_dir: Path, store_dir: Path | None = None
|
||||
) -> tuple[RunConfig, SignalStore, pl.DataFrame, Splits, torch.nn.Module, torch.device, str]:
|
||||
"""Rebuild everything an evaluation needs from a run directory.
|
||||
|
||||
Recomputes the splits from the run's config (deterministic: same
|
||||
store + same seeds -> the training-time splits; control stages
|
||||
re-apply their seeded label permutation first), reloads the probe,
|
||||
and resolves device/AMP. The returned manifest is the *working*
|
||||
manifest (permuted for control runs) so error tables agree with the
|
||||
labels the run was trained on.
|
||||
|
||||
Args:
|
||||
run_dir: Run directory (config + checkpoint).
|
||||
store_dir: Optional store override; defaults to the recorded
|
||||
``store_dir``.
|
||||
|
||||
Returns:
|
||||
``(cfg, store, manifest, splits, probe, device, amp)`` with the
|
||||
probe already moved to ``device``.
|
||||
|
||||
Raises:
|
||||
ValueError: If the recomputed genera disagree with the recorded
|
||||
genera (the store changed since training — evaluation would
|
||||
not be apples-to-apples).
|
||||
|
||||
"""
|
||||
cfg = load_run(run_dir)
|
||||
store = SignalStore(store_dir if store_dir is not None else cfg.store_dir)
|
||||
manifest = store.manifest()
|
||||
if cfg.stage == "control":
|
||||
manifest = permute_read_labels(manifest, cfg.data.genera, cfg.data.seed)
|
||||
splits = make_splits(manifest, cfg.data)
|
||||
if list(cfg.genera) != splits.genera:
|
||||
raise ValueError(
|
||||
"store genera disagree with the recorded run genera; "
|
||||
"the store must be the one the run was trained on"
|
||||
)
|
||||
probe = load_probe(run_dir, cfg)
|
||||
device = select_device(cfg.train.device)
|
||||
amp = resolve_amp(cfg.train.amp, device)
|
||||
probe = probe.to(device)
|
||||
return cfg, store, manifest, splits, probe, device, amp
|
||||
|
||||
|
||||
def _closed_payload(report: ClosedSetReport) -> dict[str, Any]:
|
||||
"""Convert a closed-set report into its JSON payload.
|
||||
|
||||
Args:
|
||||
report: The report to serialise.
|
||||
|
||||
Returns:
|
||||
Dict of the scalar metrics plus CIs as plain lists.
|
||||
|
||||
"""
|
||||
return {
|
||||
"recall_macro": report.recall_macro,
|
||||
"recall_micro": report.recall_micro,
|
||||
"precision_macro": report.precision_macro,
|
||||
"f1_macro": report.f1_macro,
|
||||
"ci": {k: [float(v[0]), float(v[1])] for k, v in report.ci.items()},
|
||||
}
|
||||
|
||||
|
||||
def evaluate_val(
|
||||
run_dir: Path, store_dir: Path | None = None, n_boot: int = 10_000
|
||||
) -> dict[str, Any]:
|
||||
"""Evaluate a run's checkpoint on its validation split.
|
||||
|
||||
Writes ``val_report.json`` (closed-set scalars + bootstrap CIs) into
|
||||
the run directory and returns the same payload.
|
||||
|
||||
Args:
|
||||
run_dir: Run directory (config + checkpoint).
|
||||
store_dir: Optional store override (default: recorded store).
|
||||
n_boot: Bootstrap resample count for CIs.
|
||||
|
||||
Returns:
|
||||
The val-report payload (run id, split, class/param counts,
|
||||
``"closed"`` block).
|
||||
|
||||
Raises:
|
||||
ValueError: If the validation split is empty or the store drifted
|
||||
(via :func:`_rehydrate`).
|
||||
|
||||
"""
|
||||
run_dir = Path(run_dir)
|
||||
cfg, store, _manifest, splits, probe, device, amp = _rehydrate(run_dir, store_dir)
|
||||
if len(splits.val) == 0:
|
||||
raise ValueError("validation split is empty")
|
||||
_, val_loader, _ = make_loaders(store, splits, cfg.train)
|
||||
val_logits, val_y = predict(probe, val_loader, device, amp)
|
||||
val_pred = val_logits.argmax(axis=1)
|
||||
report = closed_set_report(
|
||||
val_y, val_pred, splits.genera, n_boot=n_boot, seed=cfg.data.seed
|
||||
)
|
||||
payload = {
|
||||
"run_id": cfg.run_id,
|
||||
"split": "val",
|
||||
"n_classes": len(splits.genera),
|
||||
"params_realised": cfg.model.params_realised,
|
||||
"n_eval_windows": len(val_y),
|
||||
"closed": _closed_payload(report),
|
||||
}
|
||||
_write_json(run_dir / VAL_REPORT_NAME, payload)
|
||||
return payload
|
||||
|
||||
|
||||
def evaluate_test(
|
||||
run_dir: Path,
|
||||
store_dir: Path | None = None,
|
||||
trap_store_dir: Path | None = None,
|
||||
n_boot: int = 10_000,
|
||||
) -> dict[str, Any]:
|
||||
"""Evaluate a run's checkpoint on its test split (full artifact set).
|
||||
|
||||
Predicts val (for tau calibration) and test; writes
|
||||
``per_genus.parquet``, ``confusion.npz`` (matrix / genera / y_true /
|
||||
y_pred / msp), ``errors.parquet`` (per-error rows), ``open_set.json``,
|
||||
``report.json`` and — when a trap store is given —
|
||||
``trap_report.json`` (TP2: false-accept rates at tau, family-collapsed
|
||||
rates, MSP/margin histograms, top-3 retrieval, centroids).
|
||||
|
||||
Args:
|
||||
run_dir: Run directory (config + checkpoint).
|
||||
store_dir: Optional store override (default: recorded store).
|
||||
trap_store_dir: Optional eval-only trap store (its genera are
|
||||
never classes of the probe).
|
||||
n_boot: Bootstrap resample count for CIs.
|
||||
|
||||
Returns:
|
||||
The report payload: run id, class/param counts, ``"closed"`` and
|
||||
``"open_set"`` blocks, and ``"trap"`` (null when no trap store
|
||||
was supplied).
|
||||
|
||||
Raises:
|
||||
ValueError: On empty val/test splits or store drift (via
|
||||
:func:`_rehydrate`); open-set reporting errors propagate from
|
||||
:func:`~custom_models.metrics.open_set_probe_report` (e.g. no
|
||||
correct validation window — tau undefined).
|
||||
|
||||
"""
|
||||
run_dir = Path(run_dir)
|
||||
cfg, store, manifest, splits, probe, device, amp = _rehydrate(run_dir, store_dir)
|
||||
if len(splits.val) == 0 or len(splits.test) == 0:
|
||||
raise ValueError("val and test splits must both be non-empty")
|
||||
_, val_loader, test_loader = make_loaders(store, splits, cfg.train)
|
||||
val_logits, val_y = predict(probe, val_loader, device, amp)
|
||||
test_logits, test_y = predict(probe, test_loader, device, amp)
|
||||
test_pred = test_logits.argmax(axis=1)
|
||||
report = closed_set_report(
|
||||
test_y, test_pred, splits.genera, n_boot=n_boot, seed=cfg.data.seed
|
||||
)
|
||||
open_set = open_set_probe_report(val_logits, val_y, test_logits, test_y)
|
||||
test_msp = msp(test_logits)
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
report.per_genus.write_parquet(run_dir / PER_GENUS_NAME)
|
||||
np.savez_compressed(
|
||||
run_dir / CONFUSION_NAME,
|
||||
matrix=report.confusion,
|
||||
genera=np.array(splits.genera),
|
||||
y_true=test_y,
|
||||
y_pred=test_pred,
|
||||
msp=test_msp,
|
||||
)
|
||||
read_arr = manifest["read_id"].to_numpy()
|
||||
genus_arr = manifest["genus"].to_numpy()
|
||||
wid_arr = manifest["window_id"].to_numpy()
|
||||
read_by_wid = dict(zip(wid_arr.tolist(), read_arr.tolist()))
|
||||
genus_by_wid = dict(zip(wid_arr.tolist(), genus_arr.tolist()))
|
||||
wrong = np.flatnonzero(test_pred != test_y)
|
||||
errors = pl.DataFrame(
|
||||
{
|
||||
"window_id": splits.test[wrong],
|
||||
"read_id": [read_by_wid[int(w)] for w in splits.test[wrong]],
|
||||
"genus": [genus_by_wid[int(w)] for w in splits.test[wrong]],
|
||||
"pred": [splits.genera[int(p)] for p in test_pred[wrong]],
|
||||
"true_id": test_y[wrong],
|
||||
"pred_id": test_pred[wrong],
|
||||
"msp": test_msp[wrong],
|
||||
}
|
||||
)
|
||||
errors.write_parquet(run_dir / ERRORS_NAME)
|
||||
_write_json(run_dir / OPEN_SET_NAME, asdict(open_set))
|
||||
trap_payload: dict[str, Any] | None = None
|
||||
if trap_store_dir is not None:
|
||||
trap_store = SignalStore(trap_store_dir)
|
||||
trap_loader = make_full_loader(trap_store, cfg.train)
|
||||
trap_logits, _ = predict(probe, trap_loader, device, amp)
|
||||
trap_embeddings = embed_probe(probe, trap_loader, device, amp)
|
||||
trap_payload = trap_report(
|
||||
trap_store.manifest(),
|
||||
trap_logits,
|
||||
open_set.tau,
|
||||
splits.genera,
|
||||
embeddings=trap_embeddings,
|
||||
)
|
||||
_write_json(run_dir / TRAP_REPORT_NAME, trap_payload)
|
||||
payload = {
|
||||
"run_id": cfg.run_id,
|
||||
"split": "test",
|
||||
"n_classes": len(splits.genera),
|
||||
"params_realised": cfg.model.params_realised,
|
||||
"n_test_windows": len(test_y),
|
||||
"closed": _closed_payload(report),
|
||||
"open_set": asdict(open_set),
|
||||
"trap": (
|
||||
{
|
||||
"tau": trap_payload["tau"],
|
||||
"trap_fpr_at_tau": trap_payload["trap_fpr_at_tau"],
|
||||
"trap_fpr_family": trap_payload["trap_fpr_family"],
|
||||
"per_trap": trap_payload["per_trap"],
|
||||
}
|
||||
if trap_payload is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
_write_json(run_dir / REPORT_NAME, payload)
|
||||
return payload
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GateVerdict:
|
||||
"""E1-v2 gate outcome over a set of runs plus the control run.
|
||||
|
||||
Attributes:
|
||||
verdict: One of ``PASS`` / ``MARGINAL`` / ``MARGINAL-TRAP`` /
|
||||
``FAIL`` / ``INVALID`` (v2 §6 four-line table).
|
||||
best_run: Run id of the run the verdict was decided on.
|
||||
best_recall_macro: That run's macro recall.
|
||||
ci: Its 95% bootstrap CI on macro recall.
|
||||
min_params_achieving_90: Smallest realised parameter count among
|
||||
runs with recall >= 0.90 (None if none reached it).
|
||||
trap_fpr: The deciding run's ``trap_fpr_at_tau`` (None when no
|
||||
trap evaluation was attached — the PASS branch then degrades
|
||||
to MARGINAL with an explanatory rationale).
|
||||
chance: ``1 / n_classes`` from the control run's config.
|
||||
control_recall_macro: The control run's macro recall (must sit at
|
||||
``<= chance_tol * chance`` or the verdict is INVALID).
|
||||
rationale: Human-readable verdict justification quoting the gate
|
||||
table.
|
||||
|
||||
"""
|
||||
|
||||
verdict: str
|
||||
best_run: str
|
||||
best_recall_macro: float
|
||||
ci: tuple[float, float]
|
||||
min_params_achieving_90: int | None
|
||||
trap_fpr: float | None
|
||||
chance: float
|
||||
control_recall_macro: float
|
||||
rationale: str
|
||||
|
||||
|
||||
def _read_run_summary(run_dir: Path) -> dict[str, Any]:
|
||||
"""Summarise one run directory for the gate.
|
||||
|
||||
Args:
|
||||
run_dir: Directory with ``config.json`` and ``report.json``.
|
||||
|
||||
Returns:
|
||||
Dict with ``run_id``, ``params`` (realised, falling back to the
|
||||
nominal budget), ``recall_macro``, ``ci`` and ``trap_fpr``.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If either file is missing (the test command
|
||||
must run first).
|
||||
|
||||
"""
|
||||
run_dir = Path(run_dir)
|
||||
cfg = load_run(run_dir)
|
||||
report_path = run_dir / REPORT_NAME
|
||||
if not report_path.is_file():
|
||||
raise FileNotFoundError(f"run {run_dir} has no {REPORT_NAME}; run the test command first")
|
||||
report = json.loads(report_path.read_text())
|
||||
params = cfg.model.params_realised
|
||||
if params is None:
|
||||
params = cfg.model.param_budget
|
||||
trap_fpr = None
|
||||
if isinstance(report.get("trap"), dict):
|
||||
trap_fpr = report["trap"].get("trap_fpr_at_tau")
|
||||
ci = report["closed"].get("ci", {}).get("recall_macro")
|
||||
return {
|
||||
"run_id": cfg.run_id,
|
||||
"dir": str(run_dir),
|
||||
"params": int(params),
|
||||
"recall_macro": float(report["closed"]["recall_macro"]),
|
||||
"ci": (float(ci[0]), float(ci[1])) if ci else (float("nan"), float("nan")),
|
||||
"trap_fpr": trap_fpr,
|
||||
}
|
||||
|
||||
|
||||
def evaluate_gate(
|
||||
runs: Sequence[Path], control: Path, chance_tol: float = 1.5
|
||||
) -> GateVerdict:
|
||||
"""Decide the E1-v2 gate over run directories and write the verdict.
|
||||
|
||||
Logic (v2 §6, chance = 1/n_classes from the control run's config):
|
||||
the control exceeding ``chance_tol * chance`` is INVALID (suspected
|
||||
label leakage — fix before reading anything else). Otherwise, over
|
||||
the runs' *realised* params: PASS needs recall >= 0.90 at <= 5M with
|
||||
``trap_fpr_at_tau <= 0.05``; recall >= 0.90 at <= 5M with failing
|
||||
trap FPR is MARGINAL-TRAP (adopt per-class/margin thresholding);
|
||||
recall in [0.70, 0.90) at <= 5M, or 0.90 only reached at <= 20M with
|
||||
the trap criterion satisfied, is MARGINAL; anything else is FAIL.
|
||||
Missing trap evaluation degrades PASS to MARGINAL (cannot confirm the
|
||||
trap criterion). ``gate_verdict.json`` is written to the runs'
|
||||
common parent directory.
|
||||
|
||||
Args:
|
||||
runs: Run directories (each needs ``config.json`` +
|
||||
``report.json`` from the test command).
|
||||
control: The shuffled-label control run directory.
|
||||
chance_tol: Multiple of chance the control may not exceed.
|
||||
|
||||
Returns:
|
||||
The :class:`GateVerdict` (also written as JSON).
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If a run directory lacks its report.
|
||||
ValueError: If ``runs`` is empty.
|
||||
|
||||
"""
|
||||
control_dir = Path(control)
|
||||
control_summary = _read_run_summary(control_dir)
|
||||
control_cfg = load_run(control_dir)
|
||||
n_classes = len(control_cfg.genera)
|
||||
chance = 1.0 / n_classes
|
||||
control_recall = control_summary["recall_macro"]
|
||||
if control_recall > chance_tol * chance:
|
||||
verdict = GateVerdict(
|
||||
verdict="INVALID",
|
||||
best_run=control_summary["run_id"],
|
||||
best_recall_macro=control_recall,
|
||||
ci=control_summary["ci"],
|
||||
min_params_achieving_90=None,
|
||||
trap_fpr=None,
|
||||
chance=chance,
|
||||
control_recall_macro=control_recall,
|
||||
rationale=(
|
||||
f"control run recall {control_recall:.3f} exceeds {chance_tol}x chance "
|
||||
f"({chance:.3f}): suspected label leakage; fix before reading the ladder"
|
||||
),
|
||||
)
|
||||
else:
|
||||
entries = [_read_run_summary(Path(r)) for r in runs]
|
||||
if not entries:
|
||||
raise ValueError("no runs passed to the gate")
|
||||
by_5m = [e for e in entries if e["params"] <= 5_000_000]
|
||||
by_20m = [e for e in entries if e["params"] <= 20_000_000]
|
||||
best5 = max(by_5m, key=lambda e: e["recall_macro"]) if by_5m else None
|
||||
best20 = max(by_20m, key=lambda e: e["recall_macro"]) if by_20m else None
|
||||
min_params_90 = min(
|
||||
(e["params"] for e in entries if e["recall_macro"] >= 0.90), default=None
|
||||
)
|
||||
if best5 is not None and best5["recall_macro"] >= 0.90:
|
||||
chosen = best5
|
||||
if chosen["trap_fpr"] is not None and chosen["trap_fpr"] <= 0.05:
|
||||
verdict_str = "PASS"
|
||||
rationale = (
|
||||
f"best <= 5M run {chosen['run_id']} reaches recall_macro "
|
||||
f"{chosen['recall_macro']:.3f} with trap FPR {chosen['trap_fpr']:.3f} <= 0.05"
|
||||
)
|
||||
elif chosen["trap_fpr"] is not None:
|
||||
verdict_str = "MARGINAL-TRAP"
|
||||
rationale = (
|
||||
f"recall_macro {chosen['recall_macro']:.3f} at <= 5M params but trap FPR "
|
||||
f"{chosen['trap_fpr']:.3f} > 0.05: adopt per-class or margin thresholding "
|
||||
"before pod5-first is closed-set-safe"
|
||||
)
|
||||
else:
|
||||
verdict_str = "MARGINAL"
|
||||
rationale = (
|
||||
f"recall_macro {chosen['recall_macro']:.3f} at <= 5M params but no trap "
|
||||
"evaluation attached: cannot confirm the trap criterion"
|
||||
)
|
||||
elif best5 is not None and best5["recall_macro"] >= 0.70:
|
||||
chosen = best5
|
||||
verdict_str = "MARGINAL"
|
||||
rationale = (
|
||||
f"best <= 5M recall_macro {chosen['recall_macro']:.3f} in [0.70, 0.90): "
|
||||
"Stage-1 expectations drop a rank"
|
||||
)
|
||||
elif best20 is not None and best20["recall_macro"] >= 0.90:
|
||||
chosen = best20
|
||||
if chosen["trap_fpr"] is not None and chosen["trap_fpr"] <= 0.05:
|
||||
verdict_str = "MARGINAL"
|
||||
rationale = (
|
||||
f"0.90 only reached at <= 20M params ({chosen['run_id']}, "
|
||||
f"{chosen['params']} params) with trap FPR satisfied"
|
||||
)
|
||||
elif chosen["trap_fpr"] is not None:
|
||||
verdict_str = "MARGINAL-TRAP"
|
||||
rationale = (
|
||||
f"0.90 only at <= 20M params and trap FPR {chosen['trap_fpr']:.3f} > 0.05"
|
||||
)
|
||||
else:
|
||||
verdict_str = "MARGINAL"
|
||||
rationale = "0.90 only at <= 20M params; trap FPR unevaluated"
|
||||
else:
|
||||
best_overall = max(entries, key=lambda e: e["recall_macro"])
|
||||
chosen = best_overall
|
||||
verdict_str = "FAIL"
|
||||
rationale = (
|
||||
f"best recall_macro {chosen['recall_macro']:.3f} < 0.70 at <= 5M params and "
|
||||
"0.90 not reached at <= 20M: pod5-as-primary dead, demote to triage"
|
||||
)
|
||||
verdict = GateVerdict(
|
||||
verdict=verdict_str,
|
||||
best_run=chosen["run_id"],
|
||||
best_recall_macro=chosen["recall_macro"],
|
||||
ci=chosen["ci"],
|
||||
min_params_achieving_90=min_params_90,
|
||||
trap_fpr=chosen["trap_fpr"],
|
||||
chance=chance,
|
||||
control_recall_macro=control_recall,
|
||||
rationale=rationale,
|
||||
)
|
||||
dirs = [Path(r) for r in runs] + [Path(control)]
|
||||
common = os.path.commonpath([str(p.parent) for p in dirs])
|
||||
_write_json(Path(common) / GATE_NAME, asdict(verdict))
|
||||
return verdict
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Determinism helpers: seeding, RNG factories and DataLoader worker init."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def seed_everything(seed: int, deterministic: bool = True) -> None:
|
||||
"""Seed every RNG the experiment draws from.
|
||||
|
||||
Seeds ``random``, ``numpy`` and ``torch`` (CPU plus all CUDA devices).
|
||||
``CUBLAS_WORKSPACE_CONFIG`` is set (via ``setdefault``) before any CUDA
|
||||
context exists so deterministic cuBLAS is possible on CUDA >= 10.2, and
|
||||
with ``deterministic`` torch is switched to deterministic algorithms in
|
||||
warn-only mode. Idempotent; call once at every CLI entrypoint before
|
||||
anything else (GATE ZERO §2.3 contract).
|
||||
|
||||
Args:
|
||||
seed: Base seed applied to every backend (numpy is folded into its
|
||||
32-bit domain).
|
||||
deterministic: Enable ``torch.use_deterministic_algorithms(True,
|
||||
warn_only=True)``; ops without deterministic implementations warn
|
||||
instead of raising.
|
||||
|
||||
"""
|
||||
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
|
||||
random.seed(seed)
|
||||
np.random.seed(seed % (2**32))
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
if deterministic:
|
||||
torch.use_deterministic_algorithms(True, warn_only=True)
|
||||
|
||||
|
||||
def make_generator(seed: int) -> np.random.Generator:
|
||||
"""Create a fresh seeded numpy generator.
|
||||
|
||||
Args:
|
||||
seed: Seed for the new generator.
|
||||
|
||||
Returns:
|
||||
A ``numpy.random.default_rng`` generator seeded with ``seed``.
|
||||
|
||||
"""
|
||||
return np.random.default_rng(seed)
|
||||
|
||||
|
||||
def worker_init_fn(worker_id: int) -> None:
|
||||
"""Seed numpy/``random`` inside a DataLoader worker process.
|
||||
|
||||
Derives a per-worker seed from ``torch.initial_seed()`` (which the
|
||||
DataLoader already derived from the loader's own seeded generator), so
|
||||
worker-side randomness is reproducible from the run config. Pass as
|
||||
``DataLoader(worker_init_fn=...)``.
|
||||
|
||||
Args:
|
||||
worker_id: Worker index supplied by the DataLoader (unused beyond
|
||||
the signature contract; the seed comes from torch state).
|
||||
|
||||
"""
|
||||
seed = torch.initial_seed() % (2**32)
|
||||
np.random.seed(seed)
|
||||
random.seed(seed)
|
||||
@@ -0,0 +1,613 @@
|
||||
"""Signal store: pod5 extraction, the on-disk store format and its reader.
|
||||
|
||||
The store contract is the GATE ZERO schema (§4, §5.3 with the §12.1 binding
|
||||
deviations): ``manifest.parquet`` (one row per window, ``window_id`` equal to
|
||||
row order), ``shard_%05d.npy`` files holding stacked fixed-length
|
||||
median-IQR-normalised windows, and ``store_meta.json`` describing provenance
|
||||
and drop counts. A synthetic duty-cycle corpus builder provides the
|
||||
"labels survive normalisation" fixture (GATE ZERO §12.9) and smoke-test data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
|
||||
from .config import ExtractConfig
|
||||
|
||||
LABEL_COLUMNS: tuple[str, ...] = (
|
||||
"read_id",
|
||||
"genus",
|
||||
"species",
|
||||
"ref_name",
|
||||
"identity",
|
||||
"aligned_len",
|
||||
"query_len",
|
||||
"source_run",
|
||||
)
|
||||
MANIFEST_COLUMNS: tuple[str, ...] = (
|
||||
"window_id",
|
||||
"read_id",
|
||||
"genus",
|
||||
"species",
|
||||
"source_run",
|
||||
"window_index",
|
||||
"n_samples",
|
||||
)
|
||||
|
||||
STORE_META = "store_meta.json"
|
||||
MANIFEST_NAME = "manifest.parquet"
|
||||
|
||||
|
||||
def median_iqr_normalise(x: np.ndarray) -> np.ndarray:
|
||||
"""Normalise a window by its robust centre and spread: ``(x - med) / IQR``.
|
||||
|
||||
Amplitude and offset are erased (a two-level square wave always maps to
|
||||
the same two normalised levels), which is why genus identity must be
|
||||
encoded as *shape* (duty cycle) in the synthetic corpus.
|
||||
|
||||
Args:
|
||||
x: 1-D signal window, any numeric dtype.
|
||||
|
||||
Returns:
|
||||
float32 normalised window.
|
||||
|
||||
Raises:
|
||||
ValueError: If the interquartile range is zero (dead/constant
|
||||
channel).
|
||||
|
||||
"""
|
||||
x = np.asarray(x, dtype=np.float64)
|
||||
median = np.median(x)
|
||||
p75, p25 = np.percentile(x, [75, 25])
|
||||
iqr = p75 - p25
|
||||
if iqr == 0:
|
||||
raise ValueError("median_iqr normalisation undefined for a constant window")
|
||||
return ((x - median) / iqr).astype(np.float32)
|
||||
|
||||
|
||||
def median_mad_normalise(x: np.ndarray) -> np.ndarray:
|
||||
"""Normalise a window as ``(x - med) / (1.4826 * MAD)``.
|
||||
|
||||
Alternative to :func:`median_iqr_normalise` with a Gaussian-consistent
|
||||
scale estimate.
|
||||
|
||||
Args:
|
||||
x: 1-D signal window, any numeric dtype.
|
||||
|
||||
Returns:
|
||||
float32 normalised window.
|
||||
|
||||
Raises:
|
||||
ValueError: If the median absolute deviation is zero.
|
||||
|
||||
"""
|
||||
x = np.asarray(x, dtype=np.float64)
|
||||
median = np.median(x)
|
||||
mad = np.median(np.abs(x - median))
|
||||
if mad == 0:
|
||||
raise ValueError("median_mad normalisation undefined for a constant window")
|
||||
return ((x - median) / (1.4826 * mad)).astype(np.float32)
|
||||
|
||||
|
||||
_NORMALISERS = {
|
||||
"median_iqr": median_iqr_normalise,
|
||||
"median_mad": median_mad_normalise,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RawSignalRecord:
|
||||
"""One eagerly materialised pod5 read (pod5 lazy access dies with the reader).
|
||||
|
||||
Attributes:
|
||||
read_id: ONT read id string.
|
||||
signal: Calibrated picoampere trace, float32.
|
||||
num_samples: Length of ``signal`` in samples.
|
||||
sample_rate: Acquisition sample rate (Hz, default 5000 fallback).
|
||||
|
||||
"""
|
||||
|
||||
read_id: str
|
||||
signal: np.ndarray
|
||||
num_samples: int
|
||||
sample_rate: int
|
||||
|
||||
|
||||
def signal_pa(record: Any) -> np.ndarray:
|
||||
"""Return a pod5 record's signal in calibrated picoamps.
|
||||
|
||||
Prefers the reader's own ``signal_pa`` property when it exists and
|
||||
yields a non-empty 1-D array; otherwise applies the record's calibration
|
||||
manually as ``(raw + shift) * scale``.
|
||||
|
||||
Args:
|
||||
record: A ``pod5.ReadRecord`` (duck-typed; only attributes are used).
|
||||
|
||||
Returns:
|
||||
float32 picoampere trace.
|
||||
|
||||
"""
|
||||
if hasattr(record, "signal_pa"):
|
||||
try:
|
||||
values = np.asarray(record.signal_pa, dtype=np.float32)
|
||||
if values.ndim == 1 and values.size > 0:
|
||||
return values
|
||||
except (RuntimeError, ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
shift = float(getattr(record, "shift", 0.0))
|
||||
scale = float(getattr(record, "scale", 1.0))
|
||||
return (np.asarray(record.signal, dtype=np.float32) + shift) * scale
|
||||
|
||||
|
||||
def read_pod5_records(path: Path) -> Iterator[RawSignalRecord]:
|
||||
"""Yield every read of a pod5 file as eager :class:`RawSignalRecord` objects.
|
||||
|
||||
The reader context is closed when the generator is exhausted or dropped,
|
||||
so all calibration/signal access happens inside it.
|
||||
|
||||
Args:
|
||||
path: Pod5 file path.
|
||||
|
||||
Yields:
|
||||
One record per read, in file order.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If ``path`` does not exist.
|
||||
ImportError: If the optional ``pod5`` package is not installed.
|
||||
|
||||
"""
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"pod5 file not found: {path}")
|
||||
try:
|
||||
import pod5
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"the pod5 package is required to read pod5 files (pip install pod5)"
|
||||
) from exc
|
||||
with pod5.Reader(str(path)) as reader:
|
||||
for record in reader.reads():
|
||||
signal = signal_pa(record)
|
||||
run_info = getattr(record, "run_info", None)
|
||||
sample_rate = int(getattr(run_info, "sample_rate", 5000))
|
||||
yield RawSignalRecord(
|
||||
read_id=str(record.read_id),
|
||||
signal=signal,
|
||||
num_samples=int(signal.shape[0]),
|
||||
sample_rate=sample_rate,
|
||||
)
|
||||
|
||||
|
||||
def load_labels(path: Path) -> pl.DataFrame:
|
||||
"""Load and validate a labels parquet (schema: ``LABEL_COLUMNS``).
|
||||
|
||||
Args:
|
||||
path: Labels parquet path.
|
||||
|
||||
Returns:
|
||||
The validated polars DataFrame.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If ``path`` does not exist.
|
||||
ValueError: On missing columns or duplicate read ids.
|
||||
|
||||
"""
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"labels file not found: {path}")
|
||||
df = pl.read_parquet(path)
|
||||
validate_labels(df)
|
||||
return df
|
||||
|
||||
|
||||
def validate_labels(df: pl.DataFrame) -> None:
|
||||
"""Check a labels DataFrame against the label-table contract.
|
||||
|
||||
Args:
|
||||
df: Candidate labels table (columns checked order-free).
|
||||
|
||||
Raises:
|
||||
ValueError: If any of ``LABEL_COLUMNS`` is missing or ``read_id``
|
||||
contains duplicates.
|
||||
|
||||
"""
|
||||
missing = sorted(set(LABEL_COLUMNS) - set(df.columns))
|
||||
if missing:
|
||||
raise ValueError(f"labels parquet missing columns: {missing}")
|
||||
if df["read_id"].n_unique() != df.height:
|
||||
raise ValueError("labels read_id column contains duplicates")
|
||||
|
||||
|
||||
class _StoreWriter:
|
||||
"""Streaming builder for a signal store directory.
|
||||
|
||||
Buffers normalised windows, flushes them into fixed-size
|
||||
``shard_%05d.npy`` files, accumulates manifest rows and finally writes
|
||||
``manifest.parquet`` plus ``store_meta.json``.
|
||||
"""
|
||||
|
||||
def __init__(self, out_dir: Path, window_samples: int, dtype: Any, shard_windows: int) -> None:
|
||||
"""Prepare the writer and create ``out_dir``.
|
||||
|
||||
Args:
|
||||
out_dir: Store directory to create/write into.
|
||||
window_samples: Fixed window length for every emitted window.
|
||||
dtype: numpy dtype for the on-disk shards (fp16 or fp32).
|
||||
shard_windows: Windows per shard file.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``window_samples`` or ``shard_windows`` < 1.
|
||||
|
||||
"""
|
||||
if window_samples < 1:
|
||||
raise ValueError("window_samples must be >= 1")
|
||||
if shard_windows < 1:
|
||||
raise ValueError("shard_windows must be >= 1")
|
||||
self.out_dir = Path(out_dir)
|
||||
self.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.window_samples = int(window_samples)
|
||||
self.dtype = dtype
|
||||
self.shard_windows = int(shard_windows)
|
||||
self._buffer: list[np.ndarray] = []
|
||||
self._rows: list[dict[str, Any]] = []
|
||||
self._shard_idx = 0
|
||||
self._window_id = 0
|
||||
self.n_windows = 0
|
||||
self.n_reads = 0
|
||||
|
||||
def emit(
|
||||
self,
|
||||
window: np.ndarray,
|
||||
read_id: str,
|
||||
genus: str,
|
||||
species: str | None,
|
||||
source_run: str,
|
||||
window_index: int,
|
||||
) -> None:
|
||||
"""Append one normalised window and its manifest row.
|
||||
|
||||
Args:
|
||||
window: Normalised signal window of length ``window_samples``.
|
||||
read_id: Owning read id.
|
||||
genus: Genus label (never null here; labelling pre-filtered).
|
||||
species: Optional species label (may be ``None``).
|
||||
source_run: Provenance run id for the read.
|
||||
window_index: Zero-based window position within the read.
|
||||
|
||||
"""
|
||||
self._buffer.append(np.asarray(window, dtype=np.dtype(self.dtype)))
|
||||
self._rows.append(
|
||||
{
|
||||
"window_id": self._window_id,
|
||||
"read_id": read_id,
|
||||
"genus": genus,
|
||||
"species": species,
|
||||
"source_run": source_run,
|
||||
"window_index": window_index,
|
||||
"n_samples": self.window_samples,
|
||||
}
|
||||
)
|
||||
self._window_id += 1
|
||||
self.n_windows += 1
|
||||
if len(self._buffer) >= self.shard_windows:
|
||||
self._flush()
|
||||
|
||||
def count_read(self) -> None:
|
||||
"""Record that one read contributed at least one window."""
|
||||
self.n_reads += 1
|
||||
|
||||
def _flush(self) -> None:
|
||||
"""Write the buffered windows as the next shard file, if any."""
|
||||
if not self._buffer:
|
||||
return
|
||||
arr = np.stack(self._buffer).astype(self.dtype)
|
||||
np.save(self.out_dir / f"shard_{self._shard_idx:05d}.npy", arr)
|
||||
self._shard_idx += 1
|
||||
self._buffer = []
|
||||
|
||||
def finish(self, meta: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Flush the tail shard and write manifest and store meta.
|
||||
|
||||
Args:
|
||||
meta: Extra provenance fields merged into ``store_meta.json``
|
||||
(window/count fields are added automatically).
|
||||
|
||||
Returns:
|
||||
The full store meta dict that was written.
|
||||
|
||||
Raises:
|
||||
ValueError: If no window was ever emitted.
|
||||
|
||||
"""
|
||||
self._flush()
|
||||
if self.n_windows == 0:
|
||||
raise ValueError("zero windows written to the signal store")
|
||||
pl.DataFrame(self._rows, schema=list(MANIFEST_COLUMNS)).write_parquet(
|
||||
self.out_dir / MANIFEST_NAME
|
||||
)
|
||||
meta = {
|
||||
"n_windows": self.n_windows,
|
||||
"n_reads": self.n_reads,
|
||||
"shard_windows": self.shard_windows,
|
||||
**meta,
|
||||
}
|
||||
(self.out_dir / STORE_META).write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n")
|
||||
return meta
|
||||
|
||||
|
||||
def extract_store(cfg: ExtractConfig, labels: pl.DataFrame, out_dir: Path) -> Path:
|
||||
"""Build a signal store from pod5 files and a labels table.
|
||||
|
||||
For each sorted pod5 path, reads with a non-null genus label and at
|
||||
least ``min_read_samples`` samples are kept, sorted by read id, and
|
||||
their first ``max_windows_per_read`` contiguous windows (after skipping
|
||||
``skip_head_samples``) are normalised and written as shards. Fully
|
||||
deterministic: no RNG anywhere in the window choice. Drop counters
|
||||
(short/unlabelled) are reported in ``store_meta.json``.
|
||||
|
||||
Args:
|
||||
cfg: Extraction parameters (paths, windowing, normaliser, dtype,
|
||||
shard size).
|
||||
labels: Validated labels table (``LABEL_COLUMNS``); reads with null
|
||||
or empty genus are ignored.
|
||||
out_dir: Directory the store is written into (created as needed).
|
||||
|
||||
Returns:
|
||||
``out_dir`` (the store directory).
|
||||
|
||||
Raises:
|
||||
ValueError: On empty ``pod5_paths``, missing label columns,
|
||||
duplicate read ids, zero labelled reads, or zero surviving
|
||||
windows.
|
||||
FileNotFoundError: If a pod5 path is missing.
|
||||
ImportError: If the ``pod5`` package is unavailable.
|
||||
|
||||
"""
|
||||
validate_labels(labels)
|
||||
if not cfg.pod5_paths:
|
||||
raise ValueError("ExtractConfig.pod5_paths is empty")
|
||||
labelled = labels.filter(pl.col("genus").is_not_null() & (pl.col("genus") != ""))
|
||||
if labelled.height == 0:
|
||||
raise ValueError("no reads with a non-null genus in the labels table")
|
||||
info = {
|
||||
row[0]: (row[1], row[2], row[3])
|
||||
for row in zip(
|
||||
labelled["read_id"].to_list(),
|
||||
labelled["genus"].to_list(),
|
||||
labelled["species"].to_list(),
|
||||
labelled["source_run"].to_list(),
|
||||
)
|
||||
}
|
||||
normalise = _NORMALISERS[cfg.norm]
|
||||
dtype = np.dtype(np.float16 if cfg.dtype == "float16" else np.float32)
|
||||
writer = _StoreWriter(out_dir, cfg.window_samples, dtype, cfg.shard_windows)
|
||||
n_dropped_short = 0
|
||||
n_dropped_unlabelled = 0
|
||||
for pod5_path in sorted(Path(p) for p in cfg.pod5_paths):
|
||||
kept: list[RawSignalRecord] = []
|
||||
for record in read_pod5_records(pod5_path):
|
||||
if record.read_id not in info:
|
||||
n_dropped_unlabelled += 1
|
||||
continue
|
||||
if record.num_samples < cfg.min_read_samples:
|
||||
n_dropped_short += 1
|
||||
continue
|
||||
kept.append(record)
|
||||
kept.sort(key=lambda r: r.read_id)
|
||||
for record in kept:
|
||||
genus, species, source_run = info[record.read_id]
|
||||
for w in range(cfg.max_windows_per_read):
|
||||
start = cfg.skip_head_samples + w * cfg.window_samples
|
||||
if start + cfg.window_samples > record.num_samples:
|
||||
break
|
||||
window = normalise(record.signal[start : start + cfg.window_samples])
|
||||
writer.emit(window, record.read_id, genus, species, source_run, w)
|
||||
writer.count_read()
|
||||
writer.finish(
|
||||
{
|
||||
"synthetic": False,
|
||||
"window_samples": cfg.window_samples,
|
||||
"skip_head_samples": cfg.skip_head_samples,
|
||||
"max_windows_per_read": cfg.max_windows_per_read,
|
||||
"min_read_samples": cfg.min_read_samples,
|
||||
"dtype": str(cfg.dtype),
|
||||
"norm": cfg.norm,
|
||||
"pod5_paths": [str(p) for p in sorted(Path(p) for p in cfg.pod5_paths)],
|
||||
"n_labelled_reads": labelled.height,
|
||||
"n_dropped_short": n_dropped_short,
|
||||
"n_dropped_unlabelled": n_dropped_unlabelled,
|
||||
}
|
||||
)
|
||||
return Path(out_dir)
|
||||
|
||||
|
||||
def synthetic_store(
|
||||
out_dir: Path,
|
||||
n_genera: int = 11,
|
||||
reads_per_genus: int = 200,
|
||||
window_samples: int = 12_000,
|
||||
period: int | None = None,
|
||||
noise: float = 0.15,
|
||||
low: float = 1.0,
|
||||
high: float = 3.0,
|
||||
seed: int = 0,
|
||||
shard_windows: int = 8_192,
|
||||
genus_prefix: str = "genus_",
|
||||
) -> Path:
|
||||
"""Build a synthetic store whose classes are square-wave duty cycles.
|
||||
|
||||
Genus ``g`` of ``n_genera`` is encoded with duty cycle
|
||||
``(g + 1) / (n_genera + 1)`` over a square wave of ``period`` samples
|
||||
with a per-read random phase and Gaussian noise. The encoding survives
|
||||
median-IQR normalisation by construction (GATE ZERO §12.9), so a healthy
|
||||
probe trains to near-perfect recall while the shuffled-label control
|
||||
stays at chance. Windows go through the same writer/normalisation path
|
||||
as real extractions.
|
||||
|
||||
Args:
|
||||
out_dir: Store directory to create.
|
||||
n_genera: Number of classes (distinct duties).
|
||||
reads_per_genus: One window per synthetic read.
|
||||
window_samples: Window length (samples).
|
||||
period: Square-wave period; defaults to ``window_samples // 24``.
|
||||
noise: Gaussian noise sigma added to the raw levels.
|
||||
low: Raw low level (arbitrary units; erased by normalisation).
|
||||
high: Raw high level.
|
||||
seed: Seed for phase/noise draws.
|
||||
shard_windows: Windows per shard file.
|
||||
genus_prefix: Prefix for genus names (e.g. ``"trap_"`` for eval-only
|
||||
impostor corpora).
|
||||
|
||||
Returns:
|
||||
``out_dir`` (the store directory).
|
||||
|
||||
Raises:
|
||||
ValueError: On ``n_genera < 2``, ``reads_per_genus < 1`` or a
|
||||
degenerate ``period``.
|
||||
|
||||
"""
|
||||
if n_genera < 2:
|
||||
raise ValueError("synthetic corpus needs n_genera >= 2")
|
||||
if reads_per_genus < 1:
|
||||
raise ValueError("reads_per_genus must be >= 1")
|
||||
if period is None:
|
||||
period = max(4, window_samples // 24)
|
||||
if period < 2:
|
||||
raise ValueError("period must be >= 2")
|
||||
rng = np.random.default_rng(seed)
|
||||
dtype = np.dtype(np.float16)
|
||||
writer = _StoreWriter(out_dir, window_samples, dtype, shard_windows)
|
||||
for g in range(n_genera):
|
||||
genus = f"{genus_prefix}{g:02d}"
|
||||
duty = (g + 1) / (n_genera + 1)
|
||||
for r in range(reads_per_genus):
|
||||
read_id = f"{genus}_read_{r:06d}"
|
||||
phase = float(rng.uniform(0.0, period))
|
||||
offsets = (np.arange(window_samples, dtype=np.float64) + phase) % period
|
||||
signal = np.where(offsets < duty * period, high, low)
|
||||
signal = signal + rng.normal(0.0, noise, window_samples)
|
||||
window = median_iqr_normalise(signal)
|
||||
writer.emit(window, read_id, genus, None, "synthetic", 0)
|
||||
writer.count_read()
|
||||
writer.finish(
|
||||
{
|
||||
"synthetic": True,
|
||||
"window_samples": window_samples,
|
||||
"dtype": "float16",
|
||||
"norm": "median_iqr",
|
||||
"period": period,
|
||||
"noise": noise,
|
||||
"n_genera": n_genera,
|
||||
"reads_per_genus": reads_per_genus,
|
||||
"seed": seed,
|
||||
}
|
||||
)
|
||||
return Path(out_dir)
|
||||
|
||||
|
||||
class SignalStore:
|
||||
"""Random-access reader for a signal store directory.
|
||||
|
||||
Validates the manifest schema and meta on construction, then serves
|
||||
windows lazily from memory-mapped shards. Safe to share across
|
||||
forked DataLoader workers (read-only memmaps).
|
||||
"""
|
||||
|
||||
def __init__(self, store_dir: Path) -> None:
|
||||
"""Open and validate the store.
|
||||
|
||||
Args:
|
||||
store_dir: Directory holding ``manifest.parquet``,
|
||||
``store_meta.json`` and ``shard_%05d.npy`` files.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the manifest or store meta is missing.
|
||||
ValueError: If manifest columns are missing, ``window_id`` is
|
||||
not the row order, or required meta keys are absent.
|
||||
|
||||
"""
|
||||
self.dir = Path(store_dir)
|
||||
manifest_path = self.dir / MANIFEST_NAME
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(f"signal store manifest not found: {manifest_path}")
|
||||
self._manifest = pl.read_parquet(manifest_path)
|
||||
missing = sorted(set(MANIFEST_COLUMNS) - set(self._manifest.columns))
|
||||
if missing:
|
||||
raise ValueError(f"store manifest missing columns: {missing}")
|
||||
window_ids = self._manifest["window_id"].to_numpy()
|
||||
if not np.array_equal(window_ids, np.arange(self._manifest.height)):
|
||||
raise ValueError("store manifest window_id must equal the row order")
|
||||
meta_path = self.dir / STORE_META
|
||||
if not meta_path.is_file():
|
||||
raise FileNotFoundError(f"store meta not found: {meta_path}")
|
||||
self._meta = json.loads(meta_path.read_text())
|
||||
for key in ("window_samples", "shard_windows"):
|
||||
if key not in self._meta:
|
||||
raise ValueError(f"store_meta.json missing key: {key}")
|
||||
self._shard_windows = int(self._meta["shard_windows"])
|
||||
self._shards: dict[int, np.ndarray] = {}
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of windows in the store."""
|
||||
return self._manifest.height
|
||||
|
||||
def window_len(self) -> int:
|
||||
"""Return the fixed window length in samples."""
|
||||
return int(self._meta["window_samples"])
|
||||
|
||||
def manifest(self) -> pl.DataFrame:
|
||||
"""Return the (cached) manifest DataFrame, one row per window."""
|
||||
return self._manifest
|
||||
|
||||
def meta(self) -> dict[str, Any]:
|
||||
"""Return a copy of ``store_meta.json`` contents."""
|
||||
return dict(self._meta)
|
||||
|
||||
def _shard(self, index: int) -> np.ndarray:
|
||||
"""Return the (memory-mapped) shard array covering ``index``.
|
||||
|
||||
Args:
|
||||
index: Global window id.
|
||||
|
||||
Returns:
|
||||
The shard's 2-D array (opened and cached on first use).
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the shard file is absent.
|
||||
|
||||
"""
|
||||
shard = index // self._shard_windows
|
||||
if shard not in self._shards:
|
||||
path = self.dir / f"shard_{shard:05d}.npy"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"missing signal shard: {path}")
|
||||
self._shards[shard] = np.load(path, mmap_mode="r")
|
||||
return self._shards[shard]
|
||||
|
||||
def get(self, index: int) -> np.ndarray:
|
||||
"""Fetch one window as a detached float32 array.
|
||||
|
||||
Args:
|
||||
index: Global window id (0-based, the manifest row order).
|
||||
|
||||
Returns:
|
||||
float32 array of shape ``(window_len(),)``.
|
||||
|
||||
Raises:
|
||||
IndexError: If ``index`` is out of range for the store.
|
||||
|
||||
"""
|
||||
if index < 0 or index >= len(self):
|
||||
raise IndexError(f"window index out of range: {index}")
|
||||
arr = self._shard(index)
|
||||
row = index % self._shard_windows
|
||||
if row >= arr.shape[0]:
|
||||
raise IndexError(f"window {index} beyond shard rows")
|
||||
return np.asarray(arr[row], dtype=np.float32)
|
||||
@@ -0,0 +1,466 @@
|
||||
"""Training semantics: device/AMP policy, optimizer, loop and inference.
|
||||
|
||||
Encodes the training contract of the torch model spec §5: cross-entropy
|
||||
with optional label smoothing, AdamW with no decay on norm/bias
|
||||
parameters, linear-warmup cosine schedule to zero, L2 gradient clipping,
|
||||
per-epoch validation with early stopping on ``val_recall_macro`` (the
|
||||
headline gate metric) and best-epoch checkpointing to CPU tensors. All
|
||||
reported metrics are computed from CPU numpy logits so MPS/CUDA numeric
|
||||
differences cannot enter the reported numbers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from contextlib import ContextDecorator, nullcontext
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
from .config import TrainConfig
|
||||
from .data import Splits, make_loaders
|
||||
from .metrics import macro_recall
|
||||
from .models import GenusProbe
|
||||
from .store import SignalStore
|
||||
|
||||
|
||||
def select_device(requested: str | None) -> torch.device:
|
||||
"""Resolve the compute device (cuda -> mps -> cpu, never hard-coded CUDA).
|
||||
|
||||
Args:
|
||||
requested: ``None``/``""``/``"auto"`` picks the first available of
|
||||
cuda, mps, cpu; otherwise a torch device string such as
|
||||
``"cpu"``, ``"cuda"`` or ``"cuda:1"``.
|
||||
|
||||
Returns:
|
||||
The resolved ``torch.device``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``requested`` is not parseable as a device, names
|
||||
an unsupported type, or names cuda/mps on a machine where it
|
||||
is unavailable.
|
||||
|
||||
"""
|
||||
if requested is None or requested in ("", "auto"):
|
||||
if torch.cuda.is_available():
|
||||
return torch.device("cuda")
|
||||
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||
return torch.device("mps")
|
||||
return torch.device("cpu")
|
||||
try:
|
||||
device = torch.device(requested)
|
||||
except RuntimeError as exc:
|
||||
raise ValueError(f"invalid device: {requested!r}") from exc
|
||||
if device.type == "cuda" and not torch.cuda.is_available():
|
||||
raise ValueError("cuda requested but torch.cuda.is_available() is False")
|
||||
if device.type == "mps" and not (
|
||||
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
|
||||
):
|
||||
raise ValueError("mps requested but not available")
|
||||
if device.type not in ("cuda", "mps", "cpu"):
|
||||
raise ValueError(f"unsupported device type: {device.type}")
|
||||
return device
|
||||
|
||||
|
||||
def resolve_amp(amp: str, device: torch.device) -> str:
|
||||
"""Map a requested AMP policy onto what this device actually supports.
|
||||
|
||||
Policy (spec §5): ``bf16`` where supported (CUDA with bf16 tensors,
|
||||
recent MPS) else silently fp32; ``fp16`` on CUDA only (GradScaler
|
||||
handles it); ``off`` is fp32 everywhere; no autocast on CPU for
|
||||
reproducibility. Idempotent — passing an already-resolved policy
|
||||
returns it unchanged.
|
||||
|
||||
Args:
|
||||
amp: Requested policy: ``"off"``, ``"bf16"`` or ``"fp16"`` (an
|
||||
already-resolved ``"fp32"`` also round-trips).
|
||||
device: Device the policy is resolved against.
|
||||
|
||||
Returns:
|
||||
Effective policy: ``"fp32"``, ``"bf16"`` or ``"fp16"``.
|
||||
|
||||
"""
|
||||
if amp == "off":
|
||||
return "fp32"
|
||||
if device.type == "cuda":
|
||||
if amp == "bf16":
|
||||
return "bf16" if torch.cuda.is_bf16_supported() else "fp32"
|
||||
if amp == "fp16":
|
||||
return "fp16"
|
||||
if device.type == "mps" and amp == "bf16":
|
||||
try:
|
||||
with torch.autocast(device_type="mps", dtype=torch.bfloat16):
|
||||
torch.tensor([1.0]).mul(2.0)
|
||||
return "bf16"
|
||||
except (RuntimeError, TypeError, ValueError):
|
||||
return "fp32"
|
||||
if amp == "fp16" and device.type != "cuda":
|
||||
print(
|
||||
f"warning: amp={amp!r} is CUDA-only, running fp32 on {device.type}"
|
||||
)
|
||||
return "fp32"
|
||||
return "fp32"
|
||||
|
||||
|
||||
def autocast_context(policy: str, device: torch.device) -> ContextDecorator:
|
||||
"""Build the autocast context for an effective AMP policy.
|
||||
|
||||
Args:
|
||||
policy: Effective policy from :func:`resolve_amp`
|
||||
(``"fp32"``/``"bf16"``/``"fp16"``).
|
||||
device: Device the ops run on.
|
||||
|
||||
Returns:
|
||||
A usable context manager — ``torch.autocast`` for bf16/fp16 on
|
||||
accelerators, a no-op context for fp32 and for any policy on CPU.
|
||||
|
||||
"""
|
||||
if policy in ("fp32", "off") or device.type == "cpu":
|
||||
return nullcontext()
|
||||
dtype = torch.bfloat16 if policy == "bf16" else torch.float16
|
||||
return torch.autocast(device_type=device.type, dtype=dtype)
|
||||
|
||||
|
||||
def build_optimizer(
|
||||
probe: GenusProbe, cfg: TrainConfig, steps_per_epoch: int
|
||||
) -> tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]:
|
||||
"""Create AdamW with the no-decay split and warmup-cosine schedule.
|
||||
|
||||
Parameters with ``ndim <= 1`` or a ``bias`` name (all LayerNorm
|
||||
weights, every bias) go into a zero-decay group; everything else gets
|
||||
``cfg.weight_decay`` decoupled decay. The LambdaLR factor ramps
|
||||
linearly from 0 to 1 over ``warmup_frac`` of the total steps, then
|
||||
cosines from 1 to 0 across the remainder (the schedule therefore hits
|
||||
zero exactly at ``max_epochs``).
|
||||
|
||||
Args:
|
||||
probe: The probe whose parameters are optimised.
|
||||
cfg: Training config (lr, weight decay, epochs, warmup fraction).
|
||||
steps_per_epoch: Optimiser steps per epoch (``len(train_loader)``
|
||||
with ``drop_last=True``).
|
||||
|
||||
Returns:
|
||||
``(optimizer, scheduler)``; the caller steps the scheduler once
|
||||
per optimiser step.
|
||||
|
||||
"""
|
||||
decay: list[torch.nn.Parameter] = []
|
||||
no_decay: list[torch.nn.Parameter] = []
|
||||
for name, param in probe.named_parameters():
|
||||
if not param.requires_grad:
|
||||
continue
|
||||
if param.ndim <= 1 or name.endswith("bias"):
|
||||
no_decay.append(param)
|
||||
else:
|
||||
decay.append(param)
|
||||
groups = [
|
||||
{"params": no_decay, "weight_decay": 0.0},
|
||||
{"params": decay, "weight_decay": cfg.weight_decay},
|
||||
]
|
||||
optimizer = torch.optim.AdamW(groups, lr=cfg.lr)
|
||||
total_steps = max(1, steps_per_epoch * cfg.max_epochs)
|
||||
warmup_steps = max(1, int(cfg.warmup_frac * total_steps))
|
||||
|
||||
def lr_lambda(step: int) -> float:
|
||||
"""Linear-warmup cosine LR factor for LambdaLR.
|
||||
|
||||
Ramps from 0 → 1 over ``warmup_steps``, then cosines from 1 → 0
|
||||
across the remaining steps so the schedule hits zero at
|
||||
``max_epochs``.
|
||||
|
||||
Args:
|
||||
step: Current global optimiser step (0-based).
|
||||
|
||||
Returns:
|
||||
LR multiplier in [0, 1].
|
||||
|
||||
"""
|
||||
if step < warmup_steps:
|
||||
return (step + 1) / warmup_steps
|
||||
progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
|
||||
return 0.5 * (1.0 + math.cos(math.pi * min(1.0, progress)))
|
||||
|
||||
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
|
||||
return optimizer, scheduler
|
||||
|
||||
|
||||
def _mean_cross_entropy(logits: np.ndarray, y: np.ndarray) -> float:
|
||||
"""Mean cross-entropy of CPU logits (plain, no label smoothing).
|
||||
|
||||
Args:
|
||||
logits: ``(N, C)`` score matrix.
|
||||
y: ``(N,)`` int class ids.
|
||||
|
||||
Returns:
|
||||
The mean negative log-likelihood under a numerically stable
|
||||
log-softmax.
|
||||
|
||||
"""
|
||||
z = logits - logits.max(axis=1, keepdims=True)
|
||||
log_probs = z - np.log(np.exp(z).sum(axis=1, keepdims=True))
|
||||
return float(-log_probs[np.arange(len(y)), y].mean())
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainResult:
|
||||
"""Outcome of one training run.
|
||||
|
||||
Attributes:
|
||||
best_state: Weights of the best epoch (CPU tensor copies) — what
|
||||
``ckpt.pt`` stores; not final-epoch weights.
|
||||
best_epoch: 1-based epoch whose metric won (or -1 if training
|
||||
never improved).
|
||||
history: Per-epoch table: epoch, lr, train_loss, val_loss,
|
||||
val_acc, val_recall_macro, seconds (also written to
|
||||
``history.parquet``).
|
||||
seconds: Total wall time over all epochs run.
|
||||
|
||||
"""
|
||||
|
||||
best_state: dict[str, torch.Tensor]
|
||||
best_epoch: int
|
||||
history: pl.DataFrame
|
||||
seconds: float
|
||||
|
||||
|
||||
def train_probe(
|
||||
probe: GenusProbe,
|
||||
store: SignalStore,
|
||||
splits: Splits,
|
||||
cfg: TrainConfig,
|
||||
out_dir: Any,
|
||||
) -> TrainResult:
|
||||
"""Train a probe with early stopping and write ``history.parquet``.
|
||||
|
||||
Per epoch: full train pass (AMP per the resolved policy, GradScaler
|
||||
for fp16, unscaled L2 grad clipping), then validation via
|
||||
:func:`predict`; the early-stop metric improves or the patience
|
||||
counter advances. The best epoch's weights are copied to CPU and
|
||||
restored into the probe before returning. If the projected wall time
|
||||
over ``max_epochs`` exceeds 3 h, a warning is printed (the GPU-hour
|
||||
budget is a constraint, not a suggestion).
|
||||
|
||||
Args:
|
||||
probe: The probe to train (moved to the resolved device).
|
||||
store: Store the splits read from.
|
||||
splits: Split window ids and labels (val must be non-empty).
|
||||
cfg: Training semantics.
|
||||
out_dir: Directory for ``history.parquet`` (the run directory;
|
||||
created as needed).
|
||||
|
||||
Returns:
|
||||
The :class:`TrainResult` (best weights, history, timing).
|
||||
|
||||
Raises:
|
||||
ValueError: On invalid epochs/metric, a train split smaller than
|
||||
the batch size (``drop_last`` would empty it) or an empty
|
||||
train loader.
|
||||
RuntimeError: If the validation split is empty (nothing to select
|
||||
on).
|
||||
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
out_dir = pathlib.Path(out_dir)
|
||||
if cfg.max_epochs < 1:
|
||||
raise ValueError("max_epochs must be >= 1")
|
||||
if cfg.early_stop_metric not in ("val_recall_macro", "val_loss"):
|
||||
raise ValueError(f"unknown early_stop_metric: {cfg.early_stop_metric}")
|
||||
if len(splits.val) == 0:
|
||||
raise RuntimeError("validation split is empty")
|
||||
if len(splits.train) < cfg.batch_size:
|
||||
raise ValueError(
|
||||
f"train split ({len(splits.train)}) smaller than batch size ({cfg.batch_size})"
|
||||
)
|
||||
device = select_device(cfg.device)
|
||||
amp = resolve_amp(cfg.amp, device)
|
||||
train_loader, val_loader, _ = make_loaders(store, splits, cfg)
|
||||
probe = probe.to(device)
|
||||
steps_per_epoch = len(train_loader)
|
||||
if steps_per_epoch == 0:
|
||||
raise ValueError("train loader is empty")
|
||||
optimizer, scheduler = build_optimizer(probe, cfg, steps_per_epoch)
|
||||
scaler = torch.amp.GradScaler(device.type) if amp == "fp16" else None
|
||||
maximize = cfg.early_stop_metric == "val_recall_macro"
|
||||
best_metric = -math.inf if maximize else math.inf
|
||||
best_epoch = -1
|
||||
best_state: dict[str, torch.Tensor] | None = None
|
||||
bad_epochs = 0
|
||||
rows: list[dict[str, float]] = []
|
||||
n_classes = int(probe.head.out_features)
|
||||
start = time.perf_counter()
|
||||
epochs_run = 0
|
||||
for epoch in range(1, cfg.max_epochs + 1):
|
||||
epoch_start = time.perf_counter()
|
||||
probe.train()
|
||||
running_loss = 0.0
|
||||
n_seen = 0
|
||||
for windows, labels in train_loader:
|
||||
windows = windows.to(device, non_blocking=device.type == "cuda")
|
||||
labels = labels.to(device, non_blocking=device.type == "cuda")
|
||||
optimizer.zero_grad(set_to_none=True)
|
||||
with autocast_context(amp, device):
|
||||
logits = probe(windows)
|
||||
loss = F.cross_entropy(
|
||||
logits, labels, label_smoothing=cfg.label_smoothing
|
||||
)
|
||||
if scaler is not None:
|
||||
scaler.scale(loss).backward()
|
||||
scaler.unscale_(optimizer)
|
||||
if cfg.grad_clip > 0:
|
||||
torch.nn.utils.clip_grad_norm_(
|
||||
(p for g in optimizer.param_groups for p in g["params"]),
|
||||
cfg.grad_clip,
|
||||
)
|
||||
scaler.step(optimizer)
|
||||
scaler.update()
|
||||
else:
|
||||
loss.backward()
|
||||
if cfg.grad_clip > 0:
|
||||
torch.nn.utils.clip_grad_norm_(
|
||||
(p for g in optimizer.param_groups for p in g["params"]),
|
||||
cfg.grad_clip,
|
||||
)
|
||||
optimizer.step()
|
||||
scheduler.step()
|
||||
running_loss += float(loss.item()) * windows.shape[0]
|
||||
n_seen += windows.shape[0]
|
||||
train_loss = running_loss / max(1, n_seen)
|
||||
val_logits, val_y = predict(probe, val_loader, device, amp)
|
||||
val_pred = val_logits.argmax(axis=1)
|
||||
val_loss = _mean_cross_entropy(val_logits, val_y)
|
||||
val_acc = float((val_pred == val_y).mean()) if len(val_y) else float("nan")
|
||||
val_recall = macro_recall(val_y, val_pred, n_classes)
|
||||
seconds = time.perf_counter() - epoch_start
|
||||
metric = val_recall if maximize else val_loss
|
||||
improved = metric > best_metric if maximize else metric < best_metric
|
||||
if improved:
|
||||
best_metric = metric
|
||||
best_epoch = epoch
|
||||
best_state = {
|
||||
key: value.detach().to("cpu").clone()
|
||||
for key, value in probe.state_dict().items()
|
||||
}
|
||||
bad_epochs = 0
|
||||
else:
|
||||
bad_epochs += 1
|
||||
rows.append(
|
||||
{
|
||||
"epoch": epoch,
|
||||
"lr": optimizer.param_groups[0]["lr"],
|
||||
"train_loss": train_loss,
|
||||
"val_loss": val_loss,
|
||||
"val_acc": val_acc,
|
||||
"val_recall_macro": val_recall,
|
||||
"seconds": seconds,
|
||||
}
|
||||
)
|
||||
epochs_run = epoch
|
||||
if bad_epochs >= cfg.patience:
|
||||
break
|
||||
total_seconds = time.perf_counter() - start
|
||||
if best_state is None:
|
||||
raise RuntimeError("training completed without a best epoch")
|
||||
probe.load_state_dict(best_state)
|
||||
history = pl.DataFrame(rows)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
history.write_parquet(out_dir / "history.parquet")
|
||||
projected = (total_seconds / max(1, epochs_run)) * cfg.max_epochs
|
||||
if projected > 3 * 3600:
|
||||
print(
|
||||
f"warning: run projects to {projected / 3600:.1f} h over {cfg.max_epochs} epochs "
|
||||
"(budget guideline is 1-3 h per run)"
|
||||
)
|
||||
return TrainResult(
|
||||
best_state=best_state,
|
||||
best_epoch=best_epoch,
|
||||
history=history,
|
||||
seconds=total_seconds,
|
||||
)
|
||||
|
||||
|
||||
def predict(
|
||||
probe: GenusProbe,
|
||||
loader: DataLoader,
|
||||
device: torch.device,
|
||||
amp: str,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Run the probe over a loader and collect logits on CPU.
|
||||
|
||||
The module is switched to eval mode for the pass and its prior mode is
|
||||
restored on exit; gradients are disabled throughout.
|
||||
|
||||
Args:
|
||||
probe: The probe to evaluate (already on ``device``).
|
||||
loader: Any window loader (its labels are returned alongside).
|
||||
device: Compute device.
|
||||
amp: Requested or already-resolved AMP policy (re-resolved
|
||||
idempotently).
|
||||
|
||||
Returns:
|
||||
``(logits (N, C) float32 CPU, labels (N,) int64)`` in loader
|
||||
order; empty loaders yield correctly-shaped empty arrays.
|
||||
|
||||
"""
|
||||
policy = resolve_amp(amp, device)
|
||||
was_training = probe.training
|
||||
probe.eval()
|
||||
logits_chunks: list[np.ndarray] = []
|
||||
label_chunks: list[np.ndarray] = []
|
||||
with torch.no_grad():
|
||||
for windows, labels in loader:
|
||||
windows = windows.to(device, non_blocking=device.type == "cuda")
|
||||
with autocast_context(policy, device):
|
||||
out = probe(windows)
|
||||
logits_chunks.append(out.detach().to("cpu").float().numpy())
|
||||
label_chunks.append(labels.numpy())
|
||||
probe.train(was_training)
|
||||
if not logits_chunks:
|
||||
return (
|
||||
np.zeros((0, int(probe.head.out_features)), dtype=np.float32),
|
||||
np.zeros(0, dtype=np.int64),
|
||||
)
|
||||
return np.concatenate(logits_chunks), np.concatenate(label_chunks)
|
||||
|
||||
|
||||
def embed_probe(
|
||||
probe: GenusProbe,
|
||||
loader: DataLoader,
|
||||
device: torch.device,
|
||||
amp: str,
|
||||
) -> np.ndarray:
|
||||
"""Run the probe's ``embed()`` over a loader and collect embeddings.
|
||||
|
||||
Companion to :func:`predict` for the E3 reuse path (e.g. trap centroid
|
||||
vectors); same eval-mode/grad-free semantics.
|
||||
|
||||
Args:
|
||||
probe: The probe whose encoder embedding is extracted.
|
||||
loader: Any window loader (labels ignored).
|
||||
device: Compute device.
|
||||
amp: Requested or already-resolved AMP policy.
|
||||
|
||||
Returns:
|
||||
Embeddings of shape ``(N, d_embed)`` float32 on CPU.
|
||||
|
||||
"""
|
||||
policy = resolve_amp(amp, device)
|
||||
was_training = probe.training
|
||||
probe.eval()
|
||||
chunks: list[np.ndarray] = []
|
||||
with torch.no_grad():
|
||||
for windows, _ in loader:
|
||||
windows = windows.to(device, non_blocking=device.type == "cuda")
|
||||
with autocast_context(policy, device):
|
||||
out = probe.embed(windows)
|
||||
chunks.append(out.detach().to("cpu").float().numpy())
|
||||
probe.train(was_training)
|
||||
if not chunks:
|
||||
return np.zeros((0, int(probe.head.in_features)), dtype=np.float32)
|
||||
return np.concatenate(chunks)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Shared fixtures: fast synthetic stores (no pod5 needed) and determinism."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from custom_models.seed import seed_everything
|
||||
from custom_models.store import synthetic_store
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _seed_every_test() -> None:
|
||||
"""Seed all RNGs before each test (the GATE ZERO §2.3 contract)."""
|
||||
seed_everything(0)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fast_store(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
"""A small 6-genus duty-cycle store for split/config tests."""
|
||||
out = tmp_path_factory.mktemp("store") / "main"
|
||||
synthetic_store(
|
||||
out_dir=out,
|
||||
n_genera=6,
|
||||
reads_per_genus=12,
|
||||
window_samples=1200,
|
||||
shard_windows=8,
|
||||
seed=0,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def trap_store(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
"""A small eval-only impostor store (and test-split source)."""
|
||||
out = tmp_path_factory.mktemp("store") / "trap"
|
||||
synthetic_store(
|
||||
out_dir=out,
|
||||
n_genera=2,
|
||||
reads_per_genus=6,
|
||||
window_samples=1200,
|
||||
shard_windows=8,
|
||||
seed=1,
|
||||
genus_prefix="trap_",
|
||||
)
|
||||
return out
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Torch module acceptance tests: budget resolution, geometry and shapes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from custom_models.config import ModelConfig
|
||||
from custom_models.models import (
|
||||
ConvEncoder,
|
||||
GenusProbe,
|
||||
LinearAttention,
|
||||
LinearAttentionEncoder,
|
||||
SignalPatchEmbed,
|
||||
build_probe,
|
||||
count_params,
|
||||
)
|
||||
from custom_models.seed import seed_everything
|
||||
|
||||
N_CLASSES = 11
|
||||
BUDGETS = (1_000_000, 5_000_000, 20_000_000, 50_000_000, 100_000_000)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n_classes", [4, 8])
|
||||
@pytest.mark.parametrize("budget", BUDGETS)
|
||||
@pytest.mark.parametrize("arch", ["cnn", "linatt"])
|
||||
def test_build_probe_within_budget(arch: str, budget: int, n_classes: int) -> None:
|
||||
cfg = ModelConfig(arch=arch, param_budget=budget, stride=4)
|
||||
probe, realised = build_probe(cfg, n_classes)
|
||||
ceiling = int(1.1 * budget)
|
||||
assert realised.params_realised <= ceiling
|
||||
assert realised.params_realised == count_params(probe)
|
||||
assert realised.d_model is not None and realised.n_layers is not None
|
||||
assert realised.param_budget == budget
|
||||
assert probe.head.out_features == n_classes
|
||||
|
||||
|
||||
def test_build_probe_cnn_five_million_geometry_is_stable() -> None:
|
||||
cfg = ModelConfig(arch="cnn", param_budget=5_000_000, stride=4)
|
||||
probe_a, realised_a = build_probe(cfg, 11)
|
||||
_probe_b, realised_b = build_probe(cfg, 11)
|
||||
assert realised_a.d_model == realised_b.d_model
|
||||
assert realised_a.n_layers == realised_b.n_layers
|
||||
assert realised_a.params_realised == realised_b.params_realised
|
||||
assert realised_a.params_realised == count_params(probe_a)
|
||||
|
||||
|
||||
def test_build_probe_deterministic_state() -> None:
|
||||
seed_everything(0)
|
||||
cfg = ModelConfig(arch="cnn", param_budget=1_000_000, stride=4)
|
||||
probe_a, _ = build_probe(cfg, 4)
|
||||
seed_everything(0)
|
||||
probe_b, _ = build_probe(cfg, 4)
|
||||
for (a_name, a_val), (b_name, b_val) in zip(
|
||||
probe_a.state_dict().items(), probe_b.state_dict().items()
|
||||
):
|
||||
assert a_name == b_name
|
||||
assert torch.equal(a_val, b_val)
|
||||
|
||||
|
||||
def test_build_probe_no_candidate_raises() -> None:
|
||||
cfg = ModelConfig(arch="cnn", param_budget=100, stride=4)
|
||||
with pytest.raises(ValueError, match=r"no .+ candidate"):
|
||||
build_probe(cfg, 4)
|
||||
|
||||
|
||||
def test_genus_probe_rejects_single_class() -> None:
|
||||
encoder = ConvEncoder(16, 1, 4, 4, 8)
|
||||
with pytest.raises(ValueError, match="n_classes must be >= 2"):
|
||||
GenusProbe(encoder, 8, 1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("stride", "expected_tokens"), [(4, 3000), (8, 1500)])
|
||||
def test_patch_embed_token_count(stride: int, expected_tokens: int) -> None:
|
||||
embed = SignalPatchEmbed(d_model=16, patch_len=min(4, stride), stride=stride)
|
||||
x = torch.zeros(2, 12_000)
|
||||
tokens = embed(x)
|
||||
assert tokens.shape == (2, expected_tokens, 16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stride", [4, 8])
|
||||
def test_forward_shapes_and_finiteness(stride: int) -> None:
|
||||
cfg = ModelConfig(arch="cnn", param_budget=100_000, stride=stride)
|
||||
probe, realised = build_probe(cfg, N_CLASSES)
|
||||
x = torch.zeros(2, 1200)
|
||||
logits = probe(x)
|
||||
assert logits.shape == (2, N_CLASSES)
|
||||
assert torch.isfinite(logits).all()
|
||||
emb = probe.embed(x)
|
||||
assert emb.shape == (2, realised.d_embed)
|
||||
|
||||
|
||||
def test_encoder_rejects_indivisible_window() -> None:
|
||||
embed = SignalPatchEmbed(d_model=16, patch_len=4, stride=8)
|
||||
with pytest.raises(ValueError, match="not divisible"):
|
||||
embed(torch.zeros(1, 12_002))
|
||||
|
||||
|
||||
def test_no_batchnorm_and_uniform_pool_at_init() -> None:
|
||||
cfg = ModelConfig(arch="cnn", param_budget=100_000, stride=4)
|
||||
probe, _ = build_probe(cfg, N_CLASSES)
|
||||
assert not any("BatchNorm" in type(m).__name__ for m in probe.modules())
|
||||
encoder = probe.encoder
|
||||
assert isinstance(encoder, ConvEncoder)
|
||||
assert torch.count_nonzero(encoder.score.weight) == 0
|
||||
assert torch.count_nonzero(encoder.score.bias) == 0
|
||||
|
||||
|
||||
def test_conv_pool_starts_as_plain_mean() -> None:
|
||||
encoder = ConvEncoder(d_model=16, n_layers=1, patch_len=4, stride=4, d_embed=8)
|
||||
x = torch.randn(2, 1200)
|
||||
h = encoder.embed(x)
|
||||
for block in encoder.blocks:
|
||||
h = block(h)
|
||||
expected = encoder.proj(h.mean(dim=1))
|
||||
assert torch.allclose(encoder(x), expected, atol=1e-5)
|
||||
scores = torch.softmax(encoder.score(h), dim=1)
|
||||
assert torch.allclose(scores, torch.full_like(scores, 1.0 / scores.shape[1]))
|
||||
|
||||
|
||||
def test_linear_attention_heads_guard() -> None:
|
||||
with pytest.raises(ValueError, match="divisible"):
|
||||
LinearAttention(d_model=32, n_heads=5)
|
||||
with pytest.raises(ValueError, match="n_heads must be >= 1"):
|
||||
LinearAttention(d_model=32, n_heads=0)
|
||||
|
||||
|
||||
def test_linear_attention_encoder_mean_pool() -> None:
|
||||
encoder = LinearAttentionEncoder(16, 1, 4, 4, 8, n_heads=4)
|
||||
x = torch.randn(2, 1200)
|
||||
out = encoder(x)
|
||||
assert out.shape == (2, 8)
|
||||
h = encoder.embed(x)
|
||||
for block in encoder.blocks:
|
||||
h = block(h)
|
||||
assert torch.allclose(encoder.proj(h.mean(dim=1)), out, atol=1e-5)
|
||||
@@ -0,0 +1,416 @@
|
||||
"""Pipeline acceptance tests: store, splits, metrics, training and the CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from custom_models.cli import main as cli_main
|
||||
from custom_models.config import (
|
||||
DataConfig,
|
||||
ModelConfig,
|
||||
RunConfig,
|
||||
TrainConfig,
|
||||
load_config,
|
||||
run_id_for,
|
||||
save_config,
|
||||
)
|
||||
from custom_models.data import make_splits, permute_read_labels
|
||||
from custom_models.metrics import choose_tau_msp, closed_set_report, msp
|
||||
from custom_models.runner import evaluate_test, evaluate_val, train_run
|
||||
from custom_models.seed import seed_everything
|
||||
from custom_models.store import SignalStore, synthetic_store
|
||||
from custom_models.train import select_device
|
||||
|
||||
# ---------------------------------------------------------------- store
|
||||
|
||||
|
||||
def test_synthetic_store_roundtrip(tmp_path: Path) -> None:
|
||||
out = tmp_path / "s"
|
||||
synthetic_store(
|
||||
out_dir=out,
|
||||
n_genera=3,
|
||||
reads_per_genus=4,
|
||||
window_samples=1200,
|
||||
shard_windows=4,
|
||||
seed=0,
|
||||
)
|
||||
store = SignalStore(out)
|
||||
assert len(store) == 3 * 4
|
||||
assert store.window_len() == 1200
|
||||
meta = store.meta()
|
||||
assert meta["synthetic"] is True
|
||||
assert meta["n_genera"] == 3
|
||||
assert meta["n_windows"] == len(store)
|
||||
windows = [store.get(i) for i in range(len(store))]
|
||||
assert all(w.shape == (1200,) and w.dtype == np.float32 for w in windows)
|
||||
manifest = store.manifest()
|
||||
assert set(manifest.columns) >= {
|
||||
"window_id",
|
||||
"read_id",
|
||||
"genus",
|
||||
"species",
|
||||
"source_run",
|
||||
"window_index",
|
||||
"n_samples",
|
||||
}
|
||||
assert manifest["window_id"].to_list() == list(range(len(store)))
|
||||
genera = manifest["genus"].unique().sort().to_list()
|
||||
assert genera == [f"genus_{i:02d}" for i in range(3)]
|
||||
|
||||
|
||||
def test_store_labels_and_split_determinism(fast_store: Path) -> None:
|
||||
store = SignalStore(fast_store)
|
||||
manifest = store.manifest()
|
||||
selected = ("genus_00", "genus_01", "genus_02")
|
||||
cfg = DataConfig(genera=selected, min_test_windows_per_genus=0, seed=0)
|
||||
a = make_splits(manifest, cfg)
|
||||
b = make_splits(manifest, cfg)
|
||||
assert all(
|
||||
np.array_equal(x, y) for x, y in zip((a.train, a.val, a.test), (b.train, b.val, b.test))
|
||||
)
|
||||
assert a.genera == list(selected)
|
||||
# read-level purity: every window of a read shares one split
|
||||
read_arr = manifest["read_id"].to_numpy()
|
||||
wid_arr = manifest["window_id"].to_numpy()
|
||||
split_of = {int(w): 0 for w in a.train}
|
||||
split_of.update({int(w): 1 for w in a.val})
|
||||
split_of.update({int(w): 2 for w in a.test})
|
||||
by_read: dict[str, set[int]] = {}
|
||||
for wid, read_id in zip(wid_arr, read_arr):
|
||||
if int(wid) in split_of: # windows of unselected genera have no split
|
||||
by_read.setdefault(str(read_id), set()).add(split_of[int(wid)])
|
||||
assert all(len(v) == 1 for v in by_read.values())
|
||||
# a different data seed re-shuffles the read assignment
|
||||
c = make_splits(manifest, DataConfig(genera=selected, min_test_windows_per_genus=0, seed=1))
|
||||
assert not np.array_equal(a.train, c.train)
|
||||
# each read keeps its genus label; class ids map onto splits.genera
|
||||
assert len(a.labels) == len(manifest)
|
||||
assert (a.labels[a.train] >= 0).all()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- splits
|
||||
|
||||
|
||||
def test_make_splits_min_test_windows_guard(trap_store: Path) -> None:
|
||||
store = SignalStore(trap_store)
|
||||
with pytest.raises(ValueError, match="min_test_windows_per_genus"):
|
||||
make_splits(
|
||||
store.manifest(),
|
||||
DataConfig(genera=("trap_00", "trap_01"), min_test_windows_per_genus=10**6),
|
||||
)
|
||||
|
||||
|
||||
def test_make_splits_holdout_runs_guard(fast_store: Path) -> None:
|
||||
store = SignalStore(fast_store)
|
||||
manifest = store.manifest()
|
||||
# two source_runs, alternating per read: holdout picks news_run_*1 for test
|
||||
runs = [
|
||||
"news_run_0" if i % 2 == 0 else "news_run_1"
|
||||
for i, read_id in enumerate(manifest["read_id"].unique().sort().to_list())
|
||||
]
|
||||
split_map = dict(zip(manifest["read_id"].unique().sort().to_list(), runs))
|
||||
two_runs = manifest.with_columns(
|
||||
pl.col("read_id").replace_strict(split_map).alias("source_run")
|
||||
)
|
||||
assert (
|
||||
len(make_splits(two_runs, DataConfig(holdout_runs=True, min_test_windows_per_genus=1)).test)
|
||||
> 0
|
||||
)
|
||||
single_run = manifest.with_columns(pl.lit("only_run").alias("source_run"))
|
||||
with pytest.raises(ValueError, match="holdout_runs"):
|
||||
make_splits(single_run, DataConfig(holdout_runs=True, min_test_windows_per_genus=0))
|
||||
|
||||
|
||||
def test_permute_read_labels_preserves_counts(fast_store: Path) -> None:
|
||||
store = SignalStore(fast_store)
|
||||
manifest = store.manifest()
|
||||
pd = permute_read_labels(manifest, (), 3)
|
||||
counts_before = manifest["genus"].value_counts().sort("genus")
|
||||
counts_after = pd["genus"].value_counts().sort("genus")
|
||||
assert counts_before.equals(counts_after)
|
||||
# windows keep their read's label: read -> single genus before and after
|
||||
for frame in (manifest, pd):
|
||||
per_read = frame.group_by("read_id").agg(pl.col("genus").n_unique().alias("k"))
|
||||
assert per_read["k"].max() == 1
|
||||
pd2 = permute_read_labels(manifest, (), 3)
|
||||
assert pd.equals(pd2)
|
||||
with pytest.raises(ValueError, match="at least two reads"):
|
||||
permute_read_labels(manifest.filter(pl.col("read_id") == manifest["read_id"][0]), (), 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- configs
|
||||
|
||||
|
||||
def test_config_roundtrip(tmp_path: Path) -> None:
|
||||
cfg = RunConfig(
|
||||
run_id="cnn_1.0M_s4_g11",
|
||||
stage="arch_ladder",
|
||||
store_dir=Path("/data/store"),
|
||||
data=DataConfig(genera=("A", "B"), val_fraction=0.2, seed=7),
|
||||
model=ModelConfig(
|
||||
arch="cnn",
|
||||
param_budget=1_000_000,
|
||||
stride=4,
|
||||
d_model=64,
|
||||
n_layers=8,
|
||||
params_realised=912_345,
|
||||
),
|
||||
train=TrainConfig(lr=1e-3, seed=3),
|
||||
out_dir=Path("/runs"),
|
||||
genera=("A", "B"),
|
||||
notes="hello",
|
||||
)
|
||||
path = tmp_path / "config.json"
|
||||
save_config(cfg, path)
|
||||
assert load_config(path) == cfg
|
||||
|
||||
|
||||
def test_run_id_format() -> None:
|
||||
assert run_id_for("cnn", 1_000_000, 4, 11, "arch_ladder") == "cnn_1.0M_s4_g11"
|
||||
assert run_id_for("linatt", 5_000_000, 8, 6, "windows_ablation") == "linatt_5.0M_s8_g6"
|
||||
assert run_id_for("cnn", 1_000_000, 4, 11, "control") == "cnn_1.0M_s4_g11-shuf"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- metrics
|
||||
|
||||
|
||||
def test_msp_and_choose_tau() -> None:
|
||||
logits = np.array([[10.0, 0.0], [0.0, 10.0], [1.0, 1.0]])
|
||||
values = msp(logits)
|
||||
assert np.all(values >= 0.5) and values[2] == pytest.approx(0.5)
|
||||
with pytest.raises(ValueError):
|
||||
msp(np.array([1.0, 0.0]))
|
||||
with pytest.raises(ValueError):
|
||||
choose_tau_msp(np.array([]))
|
||||
arr = np.linspace(0.1, 0.9, 9)
|
||||
tau = choose_tau_msp(arr, target_known_recall=0.95)
|
||||
assert tau == pytest.approx(np.quantile(arr, 0.05))
|
||||
|
||||
|
||||
def test_closed_set_report_perfect_predictions() -> None:
|
||||
y = np.array([0, 0, 1, 1, 2])
|
||||
report = closed_set_report(y, y.copy(), ["a", "b", "c"], n_boot=50, seed=0)
|
||||
assert report.recall_macro == pytest.approx(1.0)
|
||||
assert report.recall_micro == pytest.approx(1.0)
|
||||
assert report.f1_macro == pytest.approx(1.0)
|
||||
assert (report.confusion == np.diag(np.diag(report.confusion))).all()
|
||||
assert report.ci["recall_macro"] == (1.0, 1.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- training
|
||||
|
||||
|
||||
def test_trainability_duty_cycle(tmp_path: Path) -> None:
|
||||
seed_everything(0)
|
||||
store_dir = tmp_path / "s"
|
||||
synthetic_store(
|
||||
store_dir, n_genera=2, reads_per_genus=12, window_samples=1200, shard_windows=12
|
||||
)
|
||||
cfg = RunConfig(
|
||||
run_id="",
|
||||
stage="arch_ladder",
|
||||
store_dir=store_dir,
|
||||
data=DataConfig(
|
||||
genera=(), val_fraction=0.25, test_fraction=0.25, min_test_windows_per_genus=1, seed=0
|
||||
),
|
||||
model=ModelConfig(arch="cnn", param_budget=100_000, stride=4),
|
||||
train=TrainConfig(
|
||||
max_epochs=25,
|
||||
patience=25,
|
||||
num_workers=0,
|
||||
amp="off",
|
||||
device="cpu",
|
||||
batch_size=8,
|
||||
),
|
||||
out_dir=tmp_path / "runs",
|
||||
)
|
||||
realised, _result, _splits = train_run(cfg)
|
||||
run_dir = realised.out_dir / realised.run_id
|
||||
assert (run_dir / "history.parquet").is_file()
|
||||
val_payload = evaluate_val(run_dir, n_boot=0)
|
||||
assert val_payload["closed"]["recall_macro"] >= 0.9
|
||||
|
||||
|
||||
def test_control_run_lands_at_chance(tmp_path: Path) -> None:
|
||||
seed_everything(1)
|
||||
store_dir = tmp_path / "s"
|
||||
synthetic_store(
|
||||
store_dir, n_genera=3, reads_per_genus=12, window_samples=1200, shard_windows=12
|
||||
)
|
||||
cfg = RunConfig(
|
||||
run_id="",
|
||||
stage="control",
|
||||
store_dir=store_dir,
|
||||
data=DataConfig(genera=(), val_fraction=0.25, min_test_windows_per_genus=1, seed=0),
|
||||
model=ModelConfig(arch="cnn", param_budget=100_000, stride=4),
|
||||
train=TrainConfig(
|
||||
max_epochs=4, patience=4, num_workers=0, amp="off", device="cpu", batch_size=8
|
||||
),
|
||||
out_dir=tmp_path / "runs",
|
||||
)
|
||||
realised, _result, _splits = train_run(cfg)
|
||||
assert realised.run_id.endswith("-shuf")
|
||||
payload = evaluate_test(realised.out_dir / realised.run_id, n_boot=0)
|
||||
chance = 1.0 / payload["n_classes"]
|
||||
assert 0.0 <= payload["closed"]["recall_macro"] <= 1.5 * chance
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- CLI
|
||||
|
||||
|
||||
def _cli_store_cmd(store_dir: Path) -> int:
|
||||
return cli_main(
|
||||
[
|
||||
"store",
|
||||
"--synthetic",
|
||||
"--out",
|
||||
str(store_dir),
|
||||
"--n-genera",
|
||||
"4",
|
||||
"--reads-per-genus",
|
||||
"8",
|
||||
"--window-samples",
|
||||
"1200",
|
||||
"--shard-windows",
|
||||
"64",
|
||||
"--seed",
|
||||
"0",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_cli_end_to_end(tmp_path: Path) -> None:
|
||||
store_dir = tmp_path / "cli_store"
|
||||
assert _cli_store_cmd(store_dir) == 0
|
||||
out_dir = tmp_path / "runs"
|
||||
assert (
|
||||
cli_main(
|
||||
[
|
||||
"train",
|
||||
"--store",
|
||||
str(store_dir),
|
||||
"--out-dir",
|
||||
str(out_dir),
|
||||
"--arch",
|
||||
"cnn",
|
||||
"--budget",
|
||||
"100000",
|
||||
"--batch",
|
||||
"8",
|
||||
"--epochs",
|
||||
"3",
|
||||
"--patience",
|
||||
"3",
|
||||
"--workers",
|
||||
"0",
|
||||
"--min-test-windows",
|
||||
"1",
|
||||
"--amp",
|
||||
"off",
|
||||
"--device",
|
||||
"cpu",
|
||||
"--stage",
|
||||
"arch_ladder",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
run_dir = out_dir / "cnn_0.1M_s4_g4"
|
||||
assert (run_dir / "config.json").is_file()
|
||||
assert (run_dir / "ckpt.pt").is_file()
|
||||
assert cli_main(["validate", "--run-dir", str(run_dir), "--n-boot", "50"]) == 0
|
||||
assert (run_dir / "val_report.json").is_file()
|
||||
assert cli_main(["test", "--run-dir", str(run_dir), "--n-boot", "50"]) == 0
|
||||
assert (run_dir / "report.json").is_file()
|
||||
# a control run for the gate, then the verdict
|
||||
assert (
|
||||
cli_main(
|
||||
[
|
||||
"train",
|
||||
"--store",
|
||||
str(store_dir),
|
||||
"--out-dir",
|
||||
str(out_dir),
|
||||
"--arch",
|
||||
"cnn",
|
||||
"--budget",
|
||||
"100000",
|
||||
"--batch",
|
||||
"8",
|
||||
"--epochs",
|
||||
"3",
|
||||
"--patience",
|
||||
"3",
|
||||
"--workers",
|
||||
"0",
|
||||
"--min-test-windows",
|
||||
"1",
|
||||
"--amp",
|
||||
"off",
|
||||
"--device",
|
||||
"cpu",
|
||||
"--control",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
control_dir = out_dir / "cnn_0.1M_s4_g4-shuf"
|
||||
assert cli_main(["test", "--run-dir", str(control_dir), "--n-boot", "50"]) == 0
|
||||
assert cli_main(["gate", "--runs", str(run_dir), "--control", str(control_dir)]) == 0
|
||||
assert (out_dir / "gate_verdict.json").is_file()
|
||||
|
||||
|
||||
def test_cli_test_with_trap_store(tmp_path: Path, trap_store: Path) -> None:
|
||||
store_dir = tmp_path / "s"
|
||||
synthetic_store(
|
||||
store_dir, n_genera=4, reads_per_genus=10, window_samples=1200, shard_windows=64
|
||||
)
|
||||
out_dir = tmp_path / "runs"
|
||||
assert cli_main(["train", "--store", str(store_dir), "--out-dir", str(out_dir),
|
||||
"--arch", "cnn", "--budget", "100000", "--batch", "8", "--epochs", "2",
|
||||
"--min-test-windows", "1", "--workers", "0", "--amp", "off",
|
||||
"--device", "cpu"]) == 0
|
||||
run_dir = out_dir / "cnn_0.1M_s4_g4"
|
||||
assert (
|
||||
cli_main(
|
||||
["test", "--run-dir", str(run_dir), "--trap-store", str(trap_store), "--n-boot", "50"]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
report = json.loads((run_dir / "report.json").read_text())
|
||||
assert report["trap"] is not None
|
||||
assert "trap_fpr_at_tau" in report["trap"]
|
||||
assert (run_dir / "trap_report.json").is_file()
|
||||
|
||||
|
||||
def test_build_subcommand_prints_realised(capsys: pytest.CaptureFixture) -> None:
|
||||
assert cli_main(["build", "--arch", "cnn", "--budget", "1000000", "--n-classes", "11"]) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload["params_realised"] <= int(1.1 * 1_000_000)
|
||||
assert payload["params_realised"] == payload["count_params"]
|
||||
assert payload["n_classes"] == 11
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- device/AMP
|
||||
|
||||
|
||||
def test_select_device_and_amp_policies() -> None:
|
||||
assert select_device("cpu").type == "cpu"
|
||||
assert select_device("").type == "cpu"
|
||||
with pytest.raises(ValueError):
|
||||
select_device("cuda")
|
||||
with pytest.raises(ValueError):
|
||||
select_device("not-a-device")
|
||||
device = torch.device("cpu")
|
||||
from custom_models.train import resolve_amp
|
||||
|
||||
assert resolve_amp("off", device) == "fp32"
|
||||
assert resolve_amp("bf16", device) == "fp32"
|
||||
assert resolve_amp("fp32", device) == "fp32"
|
||||
assert resolve_amp("fp16", device) == "fp32"
|
||||
@@ -0,0 +1,866 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version >= '3.12' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "custom-models"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "polars" },
|
||||
{ name = "torch", version = "2.14.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
||||
{ name = "torch", version = "2.14.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pod5" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
pod5 = [
|
||||
{ name = "pod5" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numpy", specifier = ">=2.0,<3" },
|
||||
{ name = "pod5", marker = "extra == 'dev'" },
|
||||
{ name = "pod5", marker = "extra == 'pod5'" },
|
||||
{ name = "polars", specifier = ">=1.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" },
|
||||
{ name = "torch", specifier = ">=2.6", index = "https://download.pytorch.org/whl/cpu" },
|
||||
]
|
||||
provides-extras = ["pod5", "dev"]
|
||||
|
||||
[[package]]
|
||||
name = "deprecated"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "4.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/38/88cd6eda96c40594a1e3da7d8b40f04bc40ace5a6aef9ac5cb407540f173/filelock-4.0.1.tar.gz", hash = "sha256:fdefc3f3e87716d855ae2b732c1cfd521dd99799ef2b4d00e8c0d4dcdc7cc94b", size = 238888, upload-time = "2026-09-19T01:08:15.958Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/33/af0635ab07fe83b1788a1dbe370ff3e226062495a998335cb18a1cac81aa/filelock-4.0.1-py3-none-any.whl", hash = "sha256:481a321a27bef441e23c53371c6abc8d7d16e26b97090074ba44f7538a3fd55a", size = 106219, upload-time = "2026-09-19T01:08:14.49Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsspec"
|
||||
version = "2026.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/cd/9be253869fc42e764de7f3dedd6969af7d44ff9c3375214a3442a6f3fc08/fsspec-2026.9.0.tar.gz", hash = "sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe", size = 333545, upload-time = "2026-09-18T17:50:42.825Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/c0/a98505f18594f1bce828bb159cec0fcf9860562f1a2c85913409fc8f3d9e/fsspec-2026.9.0-py3-none-any.whl", hash = "sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f", size = 221738, upload-time = "2026-09-18T17:50:41.341Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h5py"
|
||||
version = "3.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999", size = 3191673, upload-time = "2026-03-06T13:48:00.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iso8601"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lib-pod5"
|
||||
version = "0.3.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/4e/788343abb38686452b5faf058975229b9be13ab9a4af5309ad430bff2d05/lib_pod5-0.3.48-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3417cdc923aa6fe0e5969355c658535148beacfa1f88e8268004079d18aef776", size = 2867133, upload-time = "2026-09-21T12:10:26.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/d5/3e785c2ba5f2fcbc56c083b7fb79d75ded722ce662c698da56bf8c48bd81/lib_pod5-0.3.48-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:8ba21fb068120646db42ffecfe2e7f97bfd5343e3e9d139609afb337ea9b4076", size = 3821233, upload-time = "2026-09-21T12:10:28.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/df/343eb43119ef7cefcbbd7b7c63ecf086cd722fc1256a9a9aa22d336da074/lib_pod5-0.3.48-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:deb0085c3cc15f5de23133312b34ac062fb8ef92ccaceed42e82d909d0be5729", size = 4230017, upload-time = "2026-09-21T12:10:29.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/3a/2f0761c2f3f76b8c01c4d2de78c1b7231fdbe4a1e2524879f97a0f008d28/lib_pod5-0.3.48-cp311-cp311-win_amd64.whl", hash = "sha256:7f89ef6eae54df747814af2b9971d6c13acacae05f5f988373d8753d11d8892b", size = 3640682, upload-time = "2026-09-21T12:10:31.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/47/72f7acfd5587f408a9113bfbf2cc8a2a642d3fa3f5d0434241e894d3f1aa/lib_pod5-0.3.48-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:101155f03f4c4f71a0ee1f5aae915bdd4ca95f4453a6944cf0965a69f4bc60d4", size = 2868198, upload-time = "2026-09-21T12:10:33.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/af0c4249ec765bbddc550279d129fcca4f8033ba8d8a575e087982c92143/lib_pod5-0.3.48-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:494537ecccefed6fb00ea17904b4bb705e336b403b5daa054aa0e94d208508b1", size = 3821483, upload-time = "2026-09-21T12:10:35.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/675997f79cb12bafd0a14fe36b1b87fef446f5952961556ff5647f16e4a3/lib_pod5-0.3.48-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:df9180c5fc20cf37c079cd9a48e52a79abb32f297f4ba42aa1012a68e980f38f", size = 4229812, upload-time = "2026-09-21T12:10:36.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/c2/6481740a042d6b93afba6f065c2539c680c7a23ef58d2f9856312f681edc/lib_pod5-0.3.48-cp312-cp312-win_amd64.whl", hash = "sha256:d294556d2d9174c9da5f747220db21daac40a417c860a19cdd3f01eedb12c60b", size = 3633151, upload-time = "2026-09-21T12:10:37.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/0d/4f2898ca1814697396d8d1adf19a346e634973e73ffd58741839b6cdfb87/lib_pod5-0.3.48-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:94b4e3cb2a603e250ab96f00bb70f6f1c12c05d73cb107374a139a9a34985b0a", size = 2868300, upload-time = "2026-09-21T12:10:39.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/ca/de3276f4a43b0ccd9e598c61186935a20b5dca599b314de3371abfb3202f/lib_pod5-0.3.48-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:57cfb3c3093fcc97d45e80e157351b0f68083bb511606afc40226e48fece5ccf", size = 3821555, upload-time = "2026-09-21T12:10:42.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/55/9cdc96f28e6cef35c65f63ad2427b9b559d632193d269596e96ce1ac9cb2/lib_pod5-0.3.48-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2211156f13c20c83dbb768ce2992dbf3f4f72cda6e0858cadf270b5c48faa40e", size = 4229879, upload-time = "2026-09-21T12:10:44.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/edaa5410c159b5025394b0de136066db07fa956ddd4e389c3a5a4f99142f/lib_pod5-0.3.48-cp313-cp313-win_amd64.whl", hash = "sha256:d37865d81d0f002f8920d8690413e5babed6543830bf0dc86b9031e9069c6c1a", size = 3633351, upload-time = "2026-09-21T12:10:46.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/a2/4b0522e4abea7aabda02ce21b29c5afde656bf3cd6eee762ed0027568a60/lib_pod5-0.3.48-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9f0c65dbe08687940d4d46d983790c199de053c55a26e9d7079612fb75b644fc", size = 2868888, upload-time = "2026-09-21T12:10:48.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/4e/89962dc4a837624b3026253af3a664fa6b3428b458230b6464554e9b6ab5/lib_pod5-0.3.48-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:774c535d0a766b790855585da389fd32266fd4d5888837c378089caaf7cac0b7", size = 3823110, upload-time = "2026-09-21T12:10:49.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/5a/794d115508b7bf95b1aa98f598c322eb8be8b262314cc5b9c78389e1aafd/lib_pod5-0.3.48-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:56c166983e69dc59e1a50663a297f78d17609b69977d10a6fc96690872e5e196", size = 4229991, upload-time = "2026-09-21T12:10:52.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/07/97d6603e4d27e95bb51aa353a34418f494bffba946793660adc2b026ebab/lib_pod5-0.3.48-cp314-cp314-win_amd64.whl", hash = "sha256:10ba82ea4faa5971523779799cd0d3c0447174be80d872e0af811b8eb54daf1f", size = 3836102, upload-time = "2026-09-21T12:10:54.167Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "11.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version >= '3.12' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/76/3af777226b63a5e64a6b36b1ec5855c14e2b94a37096d4760e595fc43511/networkx-3.7.tar.gz", hash = "sha256:fd77a511bd90f39f3d016351345b52cf5319b813bdca01de3f755d3cca62e96a", size = 1866482, upload-time = "2026-09-21T16:45:16.974Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/cd/fe58041e9011f307c490e3e17dd48cc516448f7c698a3f2d9d9d65d7e6a8/networkx-3.7-py3-none-any.whl", hash = "sha256:e3fd2c13a7814cee3746340d8d7f8598a67f16a58bf47fb7f8793fab6efca1b0", size = 2142205, upload-time = "2026-09-21T16:45:14.609Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version >= '3.12' and sys_platform == 'darwin'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pod5"
|
||||
version = "0.3.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "deprecated" },
|
||||
{ name = "h5py" },
|
||||
{ name = "iso8601" },
|
||||
{ name = "lib-pod5" },
|
||||
{ name = "more-itertools" },
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "polars" },
|
||||
{ name = "pyarrow" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "vbz-h5py-plugin" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/41/1f985d0394536f89b3262304976751384ea9d172ae299c97c96127986e2a/pod5-0.3.48.tar.gz", hash = "sha256:841e96228dc434b9c15a1a18b23600d4d32348af9780674b23cf5c27a09c05bc", size = 66912, upload-time = "2026-09-21T12:10:57.26Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/39/2fada1b6f37d71818b97aefd7e483aeaa373c9375695373809de14cbcba3/pod5-0.3.48-py3-none-any.whl", hash = "sha256:0cd93900e5ea445bfe3820fc85d38b76ae83d3ec4a0ef1e6f7d2b1aadfef163b", size = 69921, upload-time = "2026-09-21T12:10:56.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polars"
|
||||
version = "1.44.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "polars-runtime-32" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/15/e8541eefc22fbc7ca89bcb5112298a153729f73cfbc0cf6a668e509f975c/polars-1.44.2.tar.gz", hash = "sha256:86c8e26b6c2de8c8d344bb910b74dfc47b118ac3fe0f19b44909467990a0b281", size = 766272, upload-time = "2026-09-09T07:42:08.859Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6d/3014112c7f717d1253223faa13b6db3ac3a64ed00ab2a3bc1b942bc9cdd4/polars-1.44.2-py3-none-any.whl", hash = "sha256:1bb331f17a40d9d931101533dcd33637b66edc61eb377b07020dac16a0f0377b", size = 865768, upload-time = "2026-09-09T07:40:12.053Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polars-runtime-32"
|
||||
version = "1.44.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/a1/a7eace6587b56f22cf2a21ab4d5e695db372dc23fd96accb68b1ec12660b/polars_runtime_32-1.44.2.tar.gz", hash = "sha256:b84842f7d621aaca7a52e165e19a24f89db45f8aa13744941430218419a14a67", size = 3172205, upload-time = "2026-09-09T07:42:10.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5b/a5215f82c3dd443dc5d6911b0d3e937f97056e0ef7753f7e123422481a18/polars_runtime_32-1.44.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1fd536720668ba203a16a20b08cd6b23057e407a0279cf36b2f35f879d6e3208", size = 48334014, upload-time = "2026-09-09T07:40:16.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e0/f3dc93fce4b4e99370db6a89001a1b8d3c606e3560d0d91dda809d6c6324/polars_runtime_32-1.44.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0fd43720c8222ae39919c8ff891636d53b352706087120e62f83544dd3ff782", size = 43331099, upload-time = "2026-09-09T07:40:21.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/4f/076626ce93ddd622203c4b27be2a96d034cf5b24110c52e96e6029f0ea33/polars_runtime_32-1.44.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf9b45040291dc1c6c588c837019c33557bde25ec536562a9cca9e1f6dfcc45", size = 46590853, upload-time = "2026-09-09T07:40:24.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/24/ed9982657c446dd5491b089370eea196725673570cfc61f7225a9fdd7ef0/polars_runtime_32-1.44.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1bafb441e99199a62c63bf1bbdc0ea09ee9776dbac2bf31452b5000fb1df2f7", size = 49912258, upload-time = "2026-09-09T07:40:29.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/42/5490ab360aa2406119825ad82203a5e2ff27a3a5893ca8e0b93c053a59a3/polars_runtime_32-1.44.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10c0c695a418407617b5159db7d9a21074a733e4c6d61275b6762f25cb31ca99", size = 46737310, upload-time = "2026-09-09T07:40:33.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8f/d741afb1dcd1848161189e017d27972e7e78556d8dce66b94d4235093706/polars_runtime_32-1.44.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c4a09fb14aad711526346efc0cb2015c2fd0555ce4118b6524e5debbaea65ff5", size = 49887654, upload-time = "2026-09-09T07:40:37.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e7/c61c1c7eea37705920fe7c1302d1dd80d1165db2b928f0da3eae6d1ebb75/polars_runtime_32-1.44.2-cp310-abi3-win_amd64.whl", hash = "sha256:8598e7a20efba70bb74978c7df7af7c606ff4d79b9b48fdd808250b189bc9a13", size = 51297967, upload-time = "2026-09-09T07:40:41.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/a0/d0dd0d2ec95fa328dd47055905fae53ba3cd79f11c8973326ebe75a49e4c/polars_runtime_32-1.44.2-cp310-abi3-win_arm64.whl", hash = "sha256:d51040d3ab40157f6db3c62be59cab5b80fb3c8d158924769c4982a1c8eef730", size = 44303345, upload-time = "2026-09-09T07:40:47.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyarrow"
|
||||
version = "22.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2026.3.post1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.16.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "84.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.14.0"
|
||||
source = { registry = "https://download.pytorch.org/whl/cpu" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "networkx", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sympy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1040f689e560ba6e02a0a147dfb9ec8dfdd3e4486ebec4b24dc8fdaa258fa9ba", upload-time = "2026-09-02T00:26:07Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ab7e226c18d3cbc134dfa952d18ab1df202055dd33446837c8fd050ad449e6f5", upload-time = "2026-09-02T00:26:12Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:caf6359d64c0074bcb9f8641a169239118c8165a0a0fef79110c72bfd6474bec", upload-time = "2026-09-02T00:26:15Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d0f78744000a7863cc3f1eafaf511d4b9944f079f08277de6fefe4b01f4d41b9", upload-time = "2026-09-02T00:26:19Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:dd4c297f1af83ad44368ffdaa9ed0318984567ff952bbda3d90cc300afe1e8a0", upload-time = "2026-09-02T00:26:23Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:c65f7d7783e76272bbd53e4220aa029ac14e2dc346c6641691e041202a03b0e0", upload-time = "2026-09-02T00:26:27Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:879140333148a3a5b1a14ab29a35b58c3c8db4d85c7130018a79178eb239008b", upload-time = "2026-09-02T00:26:30Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.14.0+cpu"
|
||||
source = { registry = "https://download.pytorch.org/whl/cpu" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and sys_platform != 'darwin'",
|
||||
"python_full_version < '3.12' and sys_platform != 'darwin'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "networkx", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sympy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:3fa39713d13f23563ad11636bfff76b677ab4addbc240c54e6b7e47622e8973d", upload-time = "2026-09-02T18:31:08Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:08894195f84541edcbd09072e6c53b791d4cdd09f650b05473f35718a848fd7b", upload-time = "2026-09-02T18:31:13Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:673dbf5c9bbadfffab7a386b6dd7a0c219f1408a328b7b4e86d0ae551cdafa42", upload-time = "2026-09-02T18:31:19Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:8e2c47c6556c7d5a85848634372bb2252907d411e9cad669c99406856d536eb5", upload-time = "2026-09-02T18:31:24Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:38f0a0330267b36d76b988754878d94001c1519fe3743bd92f038ce5622bea5a", upload-time = "2026-09-02T18:31:28Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:07aefe1a437b6f1b41b0cba15e16583e2f43549ec9d062fcea8e7981a78e014a", upload-time = "2026-09-02T18:31:32Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da8140c3d4a41500d29710e35bf8e66a4295ec31026487c69f54a3f583310f8d", upload-time = "2026-09-02T18:31:37Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a09987c95ec4cffdb6df798d3d641558110a334cfccce22f2f046d83142bc260", upload-time = "2026-09-02T18:31:42Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:738cd9237bf63443a5e5f9652e5115f7b0510b089636a43d4011e9fccede714f", upload-time = "2026-09-02T18:31:48Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:420e7dbed969bbd045b34105fa90d1ccf3e5c09c4e9b0d0a5f9df25df9d02d2e", upload-time = "2026-09-02T18:31:52Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:d87cfff3af33c937b88c9bc6c0dfa17f2156e2599f63447c696f2345928b518a", upload-time = "2026-09-02T18:31:55Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:092d5c12938850dfbd90a654b3c8dac34c33e300f88eb19ee6f4ef93992c6347", upload-time = "2026-09-02T18:31:59Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:160e1bc46aeded3111d2801f8ae10dc9a1b946843a7e126b4dbf5e19c5706e95", upload-time = "2026-09-02T18:32:05Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:f2ffdacd95d7090a8bdfa8426f5d1557625874b19a96ba23a41f640d4a0c9c28", upload-time = "2026-09-02T18:32:10Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:4b1b78f8b9b1393576ef03cab0e6d941a6aa562b7539c45da12d80e86c09f9ed", upload-time = "2026-09-02T18:32:13Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:266a253980047f3c037191f45d7e7ecfa838d13102141ca57b86fcc892663004", upload-time = "2026-09-02T18:32:17Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:27b34640fea7268862961b734c723873e15b1c2657b1eb84bcfac053e5841527", upload-time = "2026-09-02T18:32:23Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:f152f41dc5dc462afe0de780e451ebb47ea8b4451f8f919f9537aa8e2cbe1d7e", upload-time = "2026-09-02T18:32:29Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:ca9b3c2c652587f7b26c86d30be3648f553bd07fb3cf9b3edb645e7eafe295a3", upload-time = "2026-09-02T18:32:35Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:c1d81be03f715eca5b065fc767f73b2a26668a16d614e163a4378a3f38a5e471", upload-time = "2026-09-02T18:32:39Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a6ff46c0c0c4f5b9b399dff5e4856a98ce18e61ca14ad74b21790174c7cbde08", upload-time = "2026-09-02T18:32:43Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:2baf9cbaef964d821465df0c53a91cc01b2d0ea29ebd199e2f9c7d27a7fc70a4", upload-time = "2026-09-02T18:32:48Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:8fe5ff05cf3511daecdea986005f312f3b9cdbaf07ccf0bc42c68d01a4ae5d4d", upload-time = "2026-09-02T18:32:53Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:07d50e40232d2757be323693e38f531e04ee654d40537bc0b04e787c1e73a27a", upload-time = "2026-09-02T18:32:57Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:a8b2fa9a03d81bc083f279737fce1cd8d1ed2e267da92215260cd644dcfdd3fe", upload-time = "2026-09-02T18:33:02Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp315-cp315-win_amd64.whl", hash = "sha256:70a98493729cdd06f2735a7e4c4867e64e5d5b4eebbd6ffb841b7b034b16aff3", upload-time = "2026-09-02T18:33:07Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:e22ddf8a9fff9934fe2dd2440d45c437bb0539f2d6d24b57b26981d6dca4157c", upload-time = "2026-09-02T18:33:11Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:18b417c953e9b4d8b79294ca5184586790f0a609b3929e39258cea854b945870", upload-time = "2026-09-02T18:33:16Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.14.0%2Bcpu-cp315-cp315t-win_amd64.whl", hash = "sha256:70b02d91b68ba0652b53294f51afbe774eea0f1f52b91a20207982298af294a2", upload-time = "2026-09-02T18:33:20Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.70.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
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 = "vbz-h5py-plugin"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "h5py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/43/feb5016ab64d83f5a9cd0baa3da1f62c17a42764f44375fa59dbd576f6a0/vbz_h5py_plugin-1.0.1.tar.gz", hash = "sha256:c784458bb0aad6303474cb2f10956179116b35555803fd1154eb4ef362519341", size = 1441103, upload-time = "2022-11-10T11:53:22.603Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/dd/eab7a9cac698afd8d4d469d703303390a88570ace01da358088459a6a48d/vbz_h5py_plugin-1.0.1-py3-none-any.whl", hash = "sha256:7b8f9bf250a3c072e36bb8a84beb035468bba0536e11a9086649188f075589b1", size = 1446187, upload-time = "2022-11-10T11:53:19.893Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/a6/6375d56c44d590ef24acf0f8f5bf7ed768ff7a510b959306ec412611e90f/wrapt-2.4.1.tar.gz", hash = "sha256:fd6390aab9e8aa40c52eff3c180f098e8d9f5894b1fd4c4fd2c207067b33ed16", size = 164597, upload-time = "2026-09-10T23:12:16.811Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/c6/5e08ef1e8ad5ff518354db77b7a303666c7e0652e2a569116b5ccb72b690/wrapt-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b6ea396edcfb305698f44ef0a47b9adaca15d30386ce093ca606844f776d7236", size = 97984, upload-time = "2026-09-10T23:09:52.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/ec/e77ba77fdce732f39522382a6a9b968e5abf3471f352d51670120534e271/wrapt-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b86bfe741840cec6d8d5858b1eb53a325f564f08d7e6fb5694981961edcec230", size = 98241, upload-time = "2026-09-10T23:09:53.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/af/c54bd0bf32ef3c5f6d2d50af6bd9d88c040ef53d9f8e113e065337d76788/wrapt-2.4.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ec7aeb92e956810eedc268fe75f8c11f05d6c3d864d0995a53eb6623b3b6a51", size = 226167, upload-time = "2026-09-10T23:09:54.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/3c/934e13c7e27680760fa74e25e4b291b213fd607aa42333d883f732de6d73/wrapt-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba66eccadd4b857a845e309c08457bb71fa8300000e508e3a86cd695bf9c7515", size = 228439, upload-time = "2026-09-10T23:09:56.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/5e/7593f3fe527260ff3e8eb09ed26ff8f0bf51cda5702a75fb5969b689b4a2/wrapt-2.4.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d5c8f3b4eae814213d4097f134be03aeb23816e6358213454ec5e62e8449fb6", size = 213495, upload-time = "2026-09-10T23:09:57.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/a8/d4b4d6ac44cb2c7d4a882499786fd96758c97feb6b1539ba0ed5c823849a/wrapt-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3bc7ab495e564449f39db2f54c1033d7190c944736d525f3bd06cefaf0134df0", size = 226108, upload-time = "2026-09-10T23:09:59.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/b3/aedc046bbd8ff2333c2ce9d5267db42117221d4192ac13c66e871b528d9f/wrapt-2.4.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2e2e694057bcfd46f43e28f7b50fad8f5fec7113cc6445fdf251219539a08f28", size = 211144, upload-time = "2026-09-10T23:10:01.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/60/c01efb0e8049a9df379b04ebc589eeef995d6cf184b1d8be9f1b84b4d5f7/wrapt-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5577fdc702427698c81c8c9c3b8479cbecb3dfd0f697be8c1d09c89f5838cc", size = 215496, upload-time = "2026-09-10T23:10:02.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b5/26214472915eb447725982e2735c593131c5bae0c3fd582ad61e7b965907/wrapt-2.4.1-cp311-cp311-win32.whl", hash = "sha256:e78c0f6d66a564bab245faf170d55595df6fc237117b99d97fd838678f0906f0", size = 93493, upload-time = "2026-09-10T23:10:04.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/10/5f7d2dd40c97c5e572dce771b8d4c1c78d46a90cfc24c7b25f158253215a/wrapt-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:1f1851be0e593d65e68d7c1bc9d3b971e19fefa219f617e490d75e80e467ee14", size = 98679, upload-time = "2026-09-10T23:10:05.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/6a/f7565da9ab1323b2aa5fb4a83b919b6519cc0f4ea4b91ec8f2d25f5bcbab/wrapt-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:7ff549316d94c404999f96bb0a6993222566231ff4bede1b6f68908d1958a08f", size = 95071, upload-time = "2026-09-10T23:10:06.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/1f/a2f3225c5ecf522684c1d051aea8ce8253f240e55be75826b787777afd6c/wrapt-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7e86fbc2ac8a363ea04abf631fad82720e16b17a25020f32dbe9b24a2ed2b0e3", size = 98890, upload-time = "2026-09-10T23:10:07.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/6c/eb45660fd4d92cce11ec923f55bb2e647a6c18d30e53734eb07a3c530e31/wrapt-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24389748f0b9d5b67e478fad4fc8b3f1108422ef80716e48eead6cebcebbff08", size = 98742, upload-time = "2026-09-10T23:10:09.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/4f/17a89a580cb0082e61b8375074d5c9e5d38e4aa83ee19b6174ee472d17c2/wrapt-2.4.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30d11c289b013bf384ff1a1a6553f150d0b855901708a9bef667a5680f8247c9", size = 236301, upload-time = "2026-09-10T23:10:11.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/ca/4700eb008a34bf02de328806ddde15fc84c8d1e65d3dcafb92a935a50319/wrapt-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9356dbb59199a0e4709de35fa4a1ac1a88ef6da99711a397f5b009233faff326", size = 237805, upload-time = "2026-09-10T23:10:12.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/5d/26c1740299b29e190d5f4b4a99eb001401042a9d9ab338e3f8e1ce140ecb/wrapt-2.4.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8342f332dada211f64b74609e332d727b13315e9a83177f7918bf68c59f815f2", size = 217037, upload-time = "2026-09-10T23:10:14.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/55/ec72991153a2ae8b40238bc44cec7c3ddf7706ef6e2d314b0c6f5c7febce/wrapt-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2f86e328c482bc5383b4eda5094be0bed3617fc3076aa9225ff1a9eb6372de9b", size = 234659, upload-time = "2026-09-10T23:10:15.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/32/7cfa1e070dcda76ea56a3e252341cba1cd1e9412baf23cd7adfebf1114e2/wrapt-2.4.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4dc92697444ee380544fbb43c86612d8486529aadf917c22b5524141d4af074c", size = 214590, upload-time = "2026-09-10T23:10:16.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/10/248841cb30107f6f32c53a02662e1c3e0c7c06bea0b8ebfaaee94885dcee/wrapt-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edd03758a7578526642508b8833d43496fdfba0f64e0025dfca153a7c1777735", size = 225103, upload-time = "2026-09-10T23:10:18.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/02/5b2bf7b35b008a39939a2908e85dba3b867596e535fc9f12ddf3ba1fcaf6/wrapt-2.4.1-cp312-cp312-win32.whl", hash = "sha256:5d83e412665aeb1e854eefbf1564d0d67872d9994b502a0bce96e6ff7f4970b7", size = 93441, upload-time = "2026-09-10T23:10:19.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/2a/47be56772bfb07ef242d6e924049688e38384af2b2fc99b0f31180988448/wrapt-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:b4e7efdd476ac631a0181551fd9aace844765ea3ce2b5133b194fae4421e8ad0", size = 98808, upload-time = "2026-09-10T23:10:21.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ec/40e4c9735626afd1dc0eeb310310278361f192800503cda0f5b3d8d24db4/wrapt-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:38819761401baa2d11916d7265b82f23265f8fe5a31c431dd7c24a8863c65f88", size = 95240, upload-time = "2026-09-10T23:10:22.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/1a/9b5aa3c2391aa6d00fffa085c2219e8fc91cd4d2b9b080d2b4e4b6b93f42/wrapt-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55f36bb1461f93beaf18d818e568b5de343dd8fade7456773d201a38ee723bd3", size = 98597, upload-time = "2026-09-10T23:10:23.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/07/dc98150c2f9ee5b5fcbd841765e178ea6cc6c43733ab8d1f5181a4fb9f3d/wrapt-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:53e15cd74bd6b84d7fa90b93dda7334d85f4641fae97632de8aa61d268dfd145", size = 98842, upload-time = "2026-09-10T23:10:25.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c2/0e772a570e8d75c1b3ec45930a3023492fa68223f8f5e3485af4844c10c3/wrapt-2.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be6cdd7121adc89a6f52e3c2f4e26a2d4dcdc1c0fde47e3156234db8939e4cdc", size = 234642, upload-time = "2026-09-10T23:10:26.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/25/4ce4d02dd95ff9ed972a2fc396d04fec636daa5a4ac18cc73a3dc20aa6c3/wrapt-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03b5598edd435373278731d0d53449ce7a9626bc48d5548e4a71124ee3e526a1", size = 235484, upload-time = "2026-09-10T23:10:28.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/80/436ef1df620ef8edd9ef057b4d55a0cd704435ff66852a0ef26cbaa02a58/wrapt-2.4.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdc997819a6df4c65bdb0a1c5601c98b479ee34cc2f94ffa98a767034ad6366d", size = 214371, upload-time = "2026-09-10T23:10:29.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/2c/cc5b7503843399087db3106a7cf60d60d0900f69539e96148b9fff116291/wrapt-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3bfc6907ebed560d2d3f677c3b17bf6199679163b6c6e475035c9dca497d1697", size = 232347, upload-time = "2026-09-10T23:10:31.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/fb/17668b1ca572c44b458c09d76064d35f5f57d5ac7629248871604bef816c/wrapt-2.4.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a0c3b217332cf0c4df085fec41c126bf7507d6c1ca0efb3ba8d2b3fd234d4e73", size = 212792, upload-time = "2026-09-10T23:10:32.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2b/0c5de10d7259e07c478c44bf2fbe179c6878727d09edf0632b97884801db/wrapt-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a23b89621cfeb3329b1a290596bf402e61d7d5a647a65ca8ea735e48771b71d4", size = 223585, upload-time = "2026-09-10T23:10:34.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/36/013dce1c687f8f1c87b1e78f403d04c80ae9b2ae77de4ddba16069a3e7d1/wrapt-2.4.1-cp313-cp313-win32.whl", hash = "sha256:bc67d4872af5ab2dc1b88904097b92ac00e7658fdf010dee36807807fe882ac4", size = 93425, upload-time = "2026-09-10T23:10:35.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/1d/d53bcf5910209a45191c8bc173ff0e00830416547d8038772dc444932ee0/wrapt-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:1fe758b9c2d49138231ec3efabd106fec665f86fec50b3d02d7edd75f08a69ca", size = 98569, upload-time = "2026-09-10T23:10:37.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/1f/759d0c522918f9dfb620136622d8e1ce619570adda0de8fa2cfbb55cbb86/wrapt-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c974c36e8205255a3947dad9c2fe000431b57dd522946e56c192174a7f92a0f", size = 95272, upload-time = "2026-09-10T23:10:38.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/05/ed5aa8991c5e9969e2ca17f0e44eb324a9757f44fa982bfc13844cfe9e1d/wrapt-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6c35541cc729964c65c2b9b1f9cf317811039abc2da13b4557f20f21cdc292b", size = 98876, upload-time = "2026-09-10T23:10:40.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/5f/f8bf07c3b9a2ad01d28d5ca419e28819bb16937d129167d97c446c05875d/wrapt-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6b9df84f0a96763159cccb8e3b0ed83cc950c7c8bf82d6e45428372a805b3224", size = 99054, upload-time = "2026-09-10T23:10:41.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/7a/518e5f3ebd18472652e5332bf37728cd6634ff81a0aa568969d05cc66099/wrapt-2.4.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78af62413095d0a57077606654ce85e273deda8e2bfa28fdb04257b962d94ef2", size = 237430, upload-time = "2026-09-10T23:10:43.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2b/503835d183c1ca99268e0b5a98af8f487d528ebabb124f4003cd96e73b1f/wrapt-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60702ebc914d0bb01aa48f5c1785ceaaaa505e0a841b444a69d6ceb5de4097e", size = 237988, upload-time = "2026-09-10T23:10:44.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/9d/a11a3e1eb18ae1d9adef1c39f8e8e36fd5b86db33938bebf8e6bc5fd9b06/wrapt-2.4.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8c4b44e4be7680fc496816e824b6ed134d781c4da296e44b75399196e5c248f1", size = 218554, upload-time = "2026-09-10T23:10:45.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/61/61a6f983737ba81008561ab634373bba25b986761d3a0f0aded8352ae3e8/wrapt-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fb5b3f94258bcf71db902795f4a71151c7a74d9fe21fffa00752d2bd286866f8", size = 235476, upload-time = "2026-09-10T23:10:47.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/68/acb7cba46e0f662eaae421487807d2fed28ba52df9a00a3f04db16675e38/wrapt-2.4.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f1556a96b20d5bfdc9cb5dcd0a3ec65cbbac8a4eebcd3cd34efdc91c9519f660", size = 216454, upload-time = "2026-09-10T23:10:49.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/52/3a4dab8d4803df595de82f19775535d28c036746340c1b498fc8ecba39aa/wrapt-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:094a606d0bf1c4b847b3a743d22fc69cb164015b8c70cf6f20a534d30889ed47", size = 225346, upload-time = "2026-09-10T23:10:51.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/48/d72d051757fb7955021d8174014679f951fd5067187d2faed95520e1f8d6/wrapt-2.4.1-cp314-cp314-win32.whl", hash = "sha256:b0ee076be124406a7f97ca663c4a3ba32b6bcdd9102ca82497feaca79e9cb33c", size = 93870, upload-time = "2026-09-10T23:10:52.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/00/1a1bf4d8b60b9e7ac009969595438549ae6e33b2e3e174b78d26a833aad3/wrapt-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:d2d6f9abaa52de05090a2b4a4c1d0e858a268c4de69eec277506af9fbcccff91", size = 98910, upload-time = "2026-09-10T23:10:53.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/59/b3169c3bcdcff70a6d02ed2b6366097b083d31391d616c407efe9a943a82/wrapt-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:3152b2e94d733a9bd70dbd1c95f148266f079a70e9ffca2a9c5dc421ade1b4e6", size = 96013, upload-time = "2026-09-10T23:10:55.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/91/d29a063c2ca6e779305cd444dd5acaac60833d73b2d94c7b1d0820c27e3e/wrapt-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:37ff91b390400463ecd4d080ea823314510b6843ac53e6e35cc09bba1805d466", size = 102069, upload-time = "2026-09-10T23:10:57.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/26/7435de516099b528e2232770b459d6706002736f41d8f092bbb11ea01575/wrapt-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e0a518cac3e789443af54fc77f23e66f17d80192c281b160b42058f11fabccc5", size = 102647, upload-time = "2026-09-10T23:10:58.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a7/cca7fe0f26aa11cbbf09b2e6f4f774db167feeca43ad22fa1772afdccbad/wrapt-2.4.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ecdeb8a1ec13397421e6f925412186bd87ab86d63517e6825b6d7bccf781f26", size = 275614, upload-time = "2026-09-10T23:11:00.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/22/f3b5f4428ae679b1e2c0d4833519045d8983a9df0ad24df96b195d44dd1b/wrapt-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15c4ede3fde08723cabab0a892d4b75d35b5a6f0a51c84e6542ac0bdc0507aa", size = 287064, upload-time = "2026-09-10T23:11:02.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b8/24274ec01b6ab6d0672f4312bfa29b5f1ffd9d56f16c8da67d5683883956/wrapt-2.4.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e32c5951c36fed88b6c603dc0bab209a62dc3217bd8762413634725fe28f2f3c", size = 255440, upload-time = "2026-09-10T23:11:03.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/df/c63a36f0f03d70c7b92f60ec1ad3757088167914e1a262f5dcc7df233d51/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:898513db90d55a4ed3009312d41c12932b8163edbae3535280046d47aaff774f", size = 281681, upload-time = "2026-09-10T23:11:05.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/c7/4930b6a369d9b3da59f5ada4ae9e659e2d4a68fe72c5d1ce15e5bead2d24/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:42d01574bd4bcafc3476e77a95c6c0dd6167101991401325fc5591e2db21a91d", size = 252409, upload-time = "2026-09-10T23:11:06.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/d7/f6bc5703061598c4a2ad4f7a2efce696782be6e9d5afed714a59f5902ead/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:68a403adbeb4dd2654d6d108e43e69f0f90e6d34cb7588e0e6589109f6985e67", size = 270559, upload-time = "2026-09-10T23:11:08.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a3/952337a153e634f530079334c47dcc935b20f8ea22f364729fc2a8cf1821/wrapt-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:7cb3035b332bc9d21478600ba82c7c71e2d074ed9b4e76364297f54598792227", size = 96241, upload-time = "2026-09-10T23:11:09.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/28/48f0651547a125dd84e312b86268d4150c565a29f81ff9ca6fd81eba4be4/wrapt-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:707d2bef68deddd0fc74a81103d91b286a69ac5a9ff0f0a6dad66f8787e86697", size = 102655, upload-time = "2026-09-10T23:11:11.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a6/977441bed5e5afbffe7a789067ac2f5fd2e9f848884122a23db9ccae6a70/wrapt-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28cb1c2713b4377bf03ddcb3e76d8216e4bb334199066c6122be7eed2f72de8c", size = 98495, upload-time = "2026-09-10T23:11:12.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/1c/03bcbc3dbf6ac11231f0434f2494aa7b80657b130be1a1a639192a3e44c9/wrapt-2.4.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:96a5023fa63ca2f095f7776c8bfaa1694547577f762646c375abafd8f8ae649b", size = 98878, upload-time = "2026-09-10T23:11:14.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/26/5ad9bd421b8cdce0b2227a61f09586423765da3c284f8bbf0f69fc149fdf/wrapt-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b21924949dedc3ac63725e09b9b0a130e771975f03432789344ed3422c46008", size = 99090, upload-time = "2026-09-10T23:11:16.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/c7/fa768261966d587650004ac37bef541540014b1fe23fbf4919c497ebc98c/wrapt-2.4.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2f725af353bb3319c528ee69dbff838599426974288b281a3258d5397b18cb63", size = 237803, upload-time = "2026-09-10T23:11:17.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/37/9c1c876bd88fef8d065b3e8bf125cad9291d70ed3695cedb41b36b2453ec/wrapt-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20327e162ef7953fae56b31a43ca457f94ed1fc7a206d01e3d5c8412d1b91572", size = 238338, upload-time = "2026-09-10T23:11:19.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/5b/47b2f2f08b90d9d3b4e1790ccae0d4e4de40e73f6614c9a52465a04f02a5/wrapt-2.4.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2cedb743dbdfb9b6d4f11acd8cb6329460264429777e81ea2e86b1b72ea503af", size = 220668, upload-time = "2026-09-10T23:11:21.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/1f/da7844c7a3f7c01f3496e690e15ce9d01c0949278c64d4287ec472c90f72/wrapt-2.4.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5a54744b1193505f19016194979b773ee3754a199e9ad90db18f2e9a18fffa16", size = 235812, upload-time = "2026-09-10T23:11:23.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/af/ac4aa9f795721a54a21bac329201f7df1b199b71ef064187a266d4d29836/wrapt-2.4.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b0d38d9cc23e9781e6584f303e5c5a9f0d45b85de92ad45678c4dde8d49d9535", size = 218195, upload-time = "2026-09-10T23:11:24.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/aa/cff7670ec1cfa75b951171cfb41d45c7bdb94e928620a1599ec2b00cd2dd/wrapt-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a524ca32f0bcdde2b728d9f81f9527d4dd24b13f15d180f05d08cdc01d1cebe0", size = 225713, upload-time = "2026-09-10T23:11:26.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/c6/64f138aef50b5ea1dfacca2a023b32ba3e41ef087bbd92e8997a95f4a0b8/wrapt-2.4.1-cp315-cp315-win32.whl", hash = "sha256:47c267617551e906de72f6e7265aa3bce84c63d44513d2d2e735e943422aa0a2", size = 93880, upload-time = "2026-09-10T23:11:28.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/aa/afe7484a0f7232e87f2ebb65286c1025d43cbe1de4e4051b127b024a92f7/wrapt-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:ee437bd7fd050823ef731aad20e968ca8fe670b95e1841f5d201bfdbad4a4e96", size = 98919, upload-time = "2026-09-10T23:11:29.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/21/6865f976c6a1082ca61e2792bc6a2d6a0ed03cd750a46f837753127191f8/wrapt-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:ebf3b703752b53366fd02b7fbd8c447428e0349c9af3fc17c7a05076dbdd749a", size = 96021, upload-time = "2026-09-10T23:11:31.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/48/baf784cc9f1fe554f96daf15af06c7c466d76469d5540e357cf564755606/wrapt-2.4.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:0f5990f5db090f069fcd577cc61cc8d463db73cde132abba9033b0a264fc06d0", size = 102028, upload-time = "2026-09-10T23:11:33.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/35/fb809c95bf5512196aeda4c640aabf4e92f607340b8b2556555690a52b7a/wrapt-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef19a2590b195ac294deff8ee350a027a479afdd2ef2170ce3900af92406e110", size = 102640, upload-time = "2026-09-10T23:11:35.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/3f/bd462010ccbbe298479f320bf2bb02996706003e9c20a15790b65a186f8b/wrapt-2.4.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:432f402f9b6014403cacf9fd18a6bf089c77964330eae951a155131ab0414f5d", size = 275999, upload-time = "2026-09-10T23:11:36.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/1e/bd043b2bad2943336e090583ff6cea2f3e8776bde02f073ccf760b16eee1/wrapt-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb7c0f8bdd21e954bad89d554cfb7431c2f8179842853f199841ab90cbebe914", size = 287576, upload-time = "2026-09-10T23:11:38.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/d9/9937bbd61ca4e7f58a7e8a38f5ff3562334f741de51b87f0e031df441254/wrapt-2.4.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e82a1669d63b79a2b53041bbb6ea096b5763cab3ca698ed7ac244b3acb7cd51", size = 256763, upload-time = "2026-09-10T23:11:40.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a8/0f78cc7e7fc6313cdba32df06f4b29a45a4a2aed3499040d6c1628d3f339/wrapt-2.4.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b19e71c914c435d2c5652caea386c9bf2807979e957183f5bb06d5ed5fa8b55e", size = 282244, upload-time = "2026-09-10T23:11:42.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/0d/30317d738b9898a7fbedc193657f151a468b0fa7235f4abd58a646a99cd8/wrapt-2.4.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a1e4870d3368c6c918f38e308d5dcea970ea5b908ce889c21412f3a517ebf0c3", size = 254153, upload-time = "2026-09-10T23:11:44.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/53/fe1f251a89f471ca7c5e17f2edbd8efd48fea6f1c08430dcf60986ea4183/wrapt-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2286e8e4a937966706463d1bcea30dfefee529e6e73e097a18e9e066a07ae6c2", size = 271234, upload-time = "2026-09-10T23:11:46.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/7c/d63758cd7fa0d18c415a87312a07cdd9f0102eb824aaab5c7450b3ec08e5/wrapt-2.4.1-cp315-cp315t-win32.whl", hash = "sha256:80afa3b7010e82899044a2a189c468a0cd8c89980398a59ba3b96b451a6dc5e9", size = 96236, upload-time = "2026-09-10T23:11:47.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/2e/1785e6d2696db08b07c053b1f4ccb2b5e8f9d12e9bf2c7497693be6b4504/wrapt-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3593b43fabab6b59fe77e38f7e40974aae120c30cea3fd1ceaea6621ee96be00", size = 102662, upload-time = "2026-09-10T23:11:49.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/fd/3f4ac5c4754948355e050f952ae7a64cdca94891e49fe9927c3f90a73334/wrapt-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ac1939ccf3e1c33f463706fbf52db5075f5eefb6042bcc1f9cf48c2c20ef478c", size = 98500, upload-time = "2026-09-10T23:11:51.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a2/edcfc8d9a30375791b775715f8501364588f65b494ec4f6930568a19e765/wrapt-2.4.1-py3-none-any.whl", hash = "sha256:1e84ec5d89a0a07a0ef6bcd343f5c8ecdc95601d71de3058cdc63274e86c193c", size = 75317, upload-time = "2026-09-10T23:12:14.82Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user