diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000000000000000000000000000000000..1eafae8620a117e111e3b3309b79c95df2d31754 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,14 @@ +cff-version: 1.2.0 +title: "SimplexUQ code artifact" +message: "If you use this benchmark code, please cite the accompanying benchmark paper." +type: software +version: 0.1.0 +authors: + - family-names: "Authors" + given-names: "Anonymous" +abstract: "Executable code artifact for reproducing the SimplexUQ benchmark figures and tables from frozen derived arrays." +keywords: + - simplex + - conformal prediction + - benchmark +license: "other" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..9d176538c4ce6a0e039355fef612d8fc6b85dae2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +# Code Artifact License Notice + +This anonymous code bundle is provided for NeurIPS E&D review and benchmark +reproduction. It does not grant rights to redistribute restricted source +datasets or raw API outputs. Use the code together with the provenance and +restricted-asset notes shipped in the paired SimplexTasks-12 data bundle. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..65df45010b0fc8e67816a9c9aa50c7d31fa81664 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# simplexuq-code + +Anonymous code bundle for the SimplexUQ benchmark. + +This repository is meant to be paired with the `SimplexTasks-12-data` dataset +artifact. It contains: + +- `src/` benchmark logic and utility code +- `scripts/` benchmark runners and figure/table reproducers +- `rebuild/` task-specific rebuild notes for restricted assets +- `configs/` synthetic, real, and method configuration files +- `docs/` reviewer-facing quickstart and release notes + +Typical usage: + +```bash +python scripts/check_artifact_integrity.py +python scripts/reproduce_tables.py +python scripts/reproduce_figures.py +``` diff --git a/configs/methods/fullcp.yaml b/configs/methods/fullcp.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f103e1f5d76abc63cfe523c271f14afbd354f286 --- /dev/null +++ b/configs/methods/fullcp.yaml @@ -0,0 +1,4 @@ +method: fullcp +family: exact / local-scale reference +paper_label: FullCP +validity: Exact marginal, expensive in large settings diff --git a/configs/methods/global.yaml b/configs/methods/global.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ada6e8c3679f5cd24e91ec1dfe2446da827135e8 --- /dev/null +++ b/configs/methods/global.yaml @@ -0,0 +1,4 @@ +method: global +family: split +paper_label: Global +validity: Exact marginal under exchangeability diff --git a/configs/methods/jackknife_plus.yaml b/configs/methods/jackknife_plus.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f4a8481ba1a9ee167bd0e87b6e9f303e787e6720 --- /dev/null +++ b/configs/methods/jackknife_plus.yaml @@ -0,0 +1,4 @@ +method: jackknife_plus +family: leave-one-out reference +paper_label: Jackknife+ +validity: Approximate or exact depending on setting; used as a reference in this benchmark diff --git a/configs/methods/oneshot.yaml b/configs/methods/oneshot.yaml new file mode 100644 index 0000000000000000000000000000000000000000..47aa60e7db293940826a7698863382d13322daf6 --- /dev/null +++ b/configs/methods/oneshot.yaml @@ -0,0 +1,4 @@ +method: oneshot +family: diagnostic normalization +paper_label: OneShot +validity: No general exchangeability guarantee diff --git a/configs/methods/partition.yaml b/configs/methods/partition.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fd94e12050fa0a582b65138010079d377591177d --- /dev/null +++ b/configs/methods/partition.yaml @@ -0,0 +1,4 @@ +method: partition +family: group-wise +paper_label: Mondrian +validity: Exact within fixed groups diff --git a/configs/methods/trainres.yaml b/configs/methods/trainres.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7db0f02ae72377bbb8f2534816a1da9ce667e395 --- /dev/null +++ b/configs/methods/trainres.yaml @@ -0,0 +1,5 @@ +method: trainres +family: training-residual normalization +paper_label: TrainRes +validity: Can retain marginal validity under strong conditions but may misallocate + badly diff --git a/configs/methods/twostage.yaml b/configs/methods/twostage.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9c1e09d60d3e87ab762fd994f195c94eeb8cfa77 --- /dev/null +++ b/configs/methods/twostage.yaml @@ -0,0 +1,4 @@ +method: twostage +family: normalized split +paper_label: TwoStage +validity: Exact marginal when the scale fit is independent diff --git a/configs/methods/weighted.yaml b/configs/methods/weighted.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d617c03d4df38cbbf9494f73dc1cb8a7b5032714 --- /dev/null +++ b/configs/methods/weighted.yaml @@ -0,0 +1,4 @@ +method: weighted +family: weighted conformal diagnostic +paper_label: Weighted +validity: Implementation-specific diagnostic only in this benchmark diff --git a/configs/real/affectivetext.yaml b/configs/real/affectivetext.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6539e0229c3ab27a4358a96c01266ae46a3603cc --- /dev/null +++ b/configs/real/affectivetext.yaml @@ -0,0 +1,6 @@ +task_id: affectivetext_emotions +default_score: aitchison +default_stratification: boundary +alpha: 0.1 +repetitions: 200 +benchmark_mode: fixed_predictor diff --git a/configs/real/cifar10_softmax.yaml b/configs/real/cifar10_softmax.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b39112ead45d41f526dde840012d143a66a37aec --- /dev/null +++ b/configs/real/cifar10_softmax.yaml @@ -0,0 +1,6 @@ +task_id: cifar10_softmax +default_score: tv +default_stratification: entropy +alpha: 0.1 +repetitions: 50 +benchmark_mode: fixed_predictor diff --git a/configs/real/pbmc_pseudobulk.yaml b/configs/real/pbmc_pseudobulk.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f4e80080179b4566c3f5c7177e038198aaa84122 --- /dev/null +++ b/configs/real/pbmc_pseudobulk.yaml @@ -0,0 +1,6 @@ +task_id: pbmc3k_pseudobulk +default_score: aitchison +default_stratification: boundary +alpha: 0.1 +repetitions: 200 +benchmark_mode: fixed_predictor diff --git a/configs/real/samson_unmixing.yaml b/configs/real/samson_unmixing.yaml new file mode 100644 index 0000000000000000000000000000000000000000..74c6c0e0618ad30c2b5c4397e331e2c90d49d59a --- /dev/null +++ b/configs/real/samson_unmixing.yaml @@ -0,0 +1,6 @@ +task_id: samson_unmixing +default_score: aitchison +default_stratification: boundary +alpha: 0.1 +repetitions: 50 +benchmark_mode: fixed_predictor diff --git a/configs/real/topics_20newsgroups.yaml b/configs/real/topics_20newsgroups.yaml new file mode 100644 index 0000000000000000000000000000000000000000..85439a44f66657fe3884071f588fce3a6b59e257 --- /dev/null +++ b/configs/real/topics_20newsgroups.yaml @@ -0,0 +1,6 @@ +task_id: topics_20ng +default_score: aitchison +default_stratification: entropy +alpha: 0.1 +repetitions: 50 +benchmark_mode: fixed_predictor diff --git a/configs/real/utkface_ldl.yaml b/configs/real/utkface_ldl.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a38c51ebd03f0a64e2c97f2ac1a7b05db700bce0 --- /dev/null +++ b/configs/real/utkface_ldl.yaml @@ -0,0 +1,6 @@ +task_id: utkface_age_ldl +default_score: aitchison +default_stratification: entropy +alpha: 0.1 +repetitions: 50 +benchmark_mode: fixed_predictor diff --git a/configs/synthetic/D1.yaml b/configs/synthetic/D1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3814664c8955710b8f532d791a3c9127d18d0ac8 --- /dev/null +++ b/configs/synthetic/D1.yaml @@ -0,0 +1,35 @@ +experiment: d1_homogeneous + +dgp: + name: pure_scale + K: 3 + sigma_min: 0.2 + c: 0.0 + d_x: 2 + +data: + n_train: 1000 + n_cal: 1000 + n_scale_est: 500 + n_test: 5000 + n_rep: 200 + +methods: + - global + - partition + - twostage + - oneshot + - trainres + - weighted + - oracle + +evaluation: + alpha: 0.1 + strata_method: boundary + n_strata: 5 + +weighting: + mode: inverse_sigma + source: knn_loo + +seed: 2026 diff --git a/configs/synthetic/D2.yaml b/configs/synthetic/D2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3ad33a2b7e66653e94339a25a371bc9766112502 --- /dev/null +++ b/configs/synthetic/D2.yaml @@ -0,0 +1,37 @@ +experiment: d2_pure_scale + +dgp: + name: pure_scale + K: 3 + sigma_min: 0.1 + c: 0.5 + d_x: 2 + +data: + n_train: 500 + n_cal: 500 + n_scale_est: 250 + n_test: 5000 + n_rep: 200 + +methods: + - global + - fullcp + - jackknife_plus + - partition + - twostage + - oneshot + - trainres + - weighted + - oracle + +evaluation: + alpha: 0.1 + strata_method: boundary + n_strata: 5 + +weighting: + mode: inverse_sigma + source: knn_loo + +seed: 2026 diff --git a/configs/synthetic/D3.yaml b/configs/synthetic/D3.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b69fbb19f8e1cab7b0dd789da9ff6d60f92c5a8a --- /dev/null +++ b/configs/synthetic/D3.yaml @@ -0,0 +1,36 @@ +experiment: d3_discrete_groups_aligned + +dgp: + name: discrete_groups + K: 10 + sigma_low: 0.08 + sigma_high: 0.30 + d_x: 5 + easy_classes: 5 + +data: + n_train: 500 + n_cal: 500 + n_scale_est: 250 + n_test: 5000 + n_rep: 200 + +methods: + - global + - partition + - twostage + - fullcp + - jackknife_plus + - oracle + +evaluation: + alpha: 0.1 + strata_method: argmax_group + n_strata: 2 + split_index: 5 + +weighting: + mode: inverse_sigma + source: knn_loo + +seed: 2026 diff --git a/configs/synthetic/D4.yaml b/configs/synthetic/D4.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e8e9be4d49a9f558689628923c548a9663de22ef --- /dev/null +++ b/configs/synthetic/D4.yaml @@ -0,0 +1,39 @@ +experiment: d4_model_bias + +dgp: + name: model_bias + K: 3 + sigma_min: 0.1 + c: 0.15 + d_x: 2 + bias_scale: 0.45 + bias_type: rotational + +data: + n_train: 500 + n_cal: 500 + n_scale_est: 250 + n_test: 5000 + n_rep: 200 + +methods: + - global + - fullcp + - jackknife_plus + - partition + - twostage + - oneshot + - trainres + - weighted + - oracle + +evaluation: + alpha: 0.1 + strata_method: boundary + n_strata: 5 + +weighting: + mode: inverse_sigma + source: knn_loo + +seed: 2026 diff --git a/configs/synthetic/D5.yaml b/configs/synthetic/D5.yaml new file mode 100644 index 0000000000000000000000000000000000000000..81903018443f1898b6bc89c6f91cb6845d248741 --- /dev/null +++ b/configs/synthetic/D5.yaml @@ -0,0 +1,36 @@ +experiment: d5_heavy_tail + +dgp: + name: heavy_tail + K: 3 + sigma_min: 0.1 + c: 0.5 + d_x: 2 + df: 3.0 + +data: + n_train: 500 + n_cal: 500 + n_scale_est: 250 + n_test: 5000 + n_rep: 200 + +methods: + - global + - fullcp + - jackknife_plus + - partition + - twostage + - weighted + - oracle + +evaluation: + alpha: 0.1 + strata_method: boundary + n_strata: 5 + +weighting: + mode: inverse_sigma + source: knn_loo + +seed: 2026 diff --git a/configs/synthetic/D6.yaml b/configs/synthetic/D6.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3261e4be4057ffcd2ff9a2aeef4a2804c73cb2f1 --- /dev/null +++ b/configs/synthetic/D6.yaml @@ -0,0 +1,33 @@ +experiment: d6_high_k + +dgp: + name: high_k + K: 50 + sigma_min: 0.05 + c: 0.35 + d_x: 10 + +data: + n_train: 5000 + n_cal: 5000 + n_scale_est: 2500 + n_test: 5000 + n_rep: 200 + +methods: + - global + - partition + - twostage + - weighted + - oracle + +evaluation: + alpha: 0.1 + strata_method: entropy + n_strata: 5 + +weighting: + mode: inverse_sigma + source: knn_loo + +seed: 2026 diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000000000000000000000000000000000..94d2a86194fd9fd693603cda8bf941ecd8746c36 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,14 @@ +# FAQ + +## Why are there no raw images or raw headlines here? +Because this artifact is evaluation-first and respects source-asset terms. The +benchmark runs on frozen derived arrays and rebuild metadata instead of mirroring +restricted raw assets. + +## Why are there two upload bundles? +Splitting data and code keeps the dataset artifact clean and reduces ambiguity +about what counts as the benchmark state versus what counts as execution logic. + +## What should a reviewer run first? +The code bundle's figure/table reproduction helpers. They operate on frozen +derived arrays and are the shortest path to the paper outputs. diff --git a/docs/release_contract.md b/docs/release_contract.md new file mode 100644 index 0000000000000000000000000000000000000000..964d45da9f1218c960a1e991c2d8cdab87bef664 --- /dev/null +++ b/docs/release_contract.md @@ -0,0 +1,7 @@ +# Release Contract + +This code bundle assumes that benchmark evaluation is run on frozen derived arrays. +It does not require raw-asset mirrors for the paper-level reproduction path. + +The `rebuild/` directories are only for tasks whose source assets are restricted +or inconvenient to redistribute directly. diff --git a/docs/restricted_assets.md b/docs/restricted_assets.md new file mode 100644 index 0000000000000000000000000000000000000000..f20b47d2a03cc266d8805ef43e5b3490ce29fba9 --- /dev/null +++ b/docs/restricted_assets.md @@ -0,0 +1,12 @@ +# Restricted Assets + +The following raw assets are intentionally excluded from the data bundle: + +- CIFAR-10 image archive +- UTKFace face-image archive +- Raw AffectiveText headlines +- Raw AffectiveText API responses + +Rebuild instructions and metadata are provided instead. The benchmark runner is +designed to consume frozen derived arrays, so raw mirrors are not required for +the paper-level reproducibility path. diff --git a/docs/reviewer_quickstart.md b/docs/reviewer_quickstart.md new file mode 100644 index 0000000000000000000000000000000000000000..aadb93dcdc896a416c3a4fe1506391aa1a8dc66f --- /dev/null +++ b/docs/reviewer_quickstart.md @@ -0,0 +1,19 @@ +# Reviewer Quickstart + +1. Place or symlink the `SimplexTasks-12-data` bundle next to this code bundle. +2. Create an environment from `environment.yml` or install the packages listed in + `requirements.txt`. +3. Verify the two-bundle layout: + +```bash +python scripts/check_artifact_integrity.py +``` + +4. Regenerate tables and figures from the frozen cached inputs: + +```bash +python scripts/reproduce_tables.py +python scripts/reproduce_figures.py +``` + +5. Inspect `outputs/tables/` and `outputs/figures/`. diff --git a/docs/task_limitations.md b/docs/task_limitations.md new file mode 100644 index 0000000000000000000000000000000000000000..94b2f9af752650bba3b65deb4e4f1035512312b1 --- /dev/null +++ b/docs/task_limitations.md @@ -0,0 +1,7 @@ +# Task Limitations + +- CIFAR-10 is a classification-style stress test, not a naturally continuous composition task. +- Topics is model-derived and should be read as topic-mixture evaluation rather than raw-label truth. +- PBMC is semi-synthetic and is intended as a control-style benchmark slice. +- UTKFace and AffectiveText rely on derived artifacts because the source assets are restricted. +- Samson is the cleanest natural low-dimensional composition task in the bundle. diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000000000000000000000000000000000000..a41a6c95cd731d8f8b392f3e7231afb616feae5f --- /dev/null +++ b/environment.yml @@ -0,0 +1,14 @@ +name: simplexuq-code +channels: + - conda-forge +dependencies: + - python=3.11 + - numpy>=1.24 + - scipy>=1.10 + - scikit-learn>=1.3 + - matplotlib>=3.7 + - pyyaml>=6.0 + - scanpy + - anndata + - rpy2 + - pip diff --git a/outputs/README.md b/outputs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f135f5ff1523ad50237bba15a4721f1fba96f51b --- /dev/null +++ b/outputs/README.md @@ -0,0 +1 @@ +Generated benchmark outputs go under this directory. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..139eef6342a70c80ce5da624db91e7a317a44247 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "simplexuq-code" +version = "0.1.0" +requires-python = ">=3.10,<3.14" +dependencies = [ + "numpy>=1.24", + "scipy>=1.10", + "scikit-learn>=1.3", + "matplotlib>=3.7", + "pyyaml>=6.0", +] + +[project.optional-dependencies] +bio = ["scanpy", "anndata", "rpy2"] # for deconvolution experiments +r = ["rpy2"] # for R integration (visualization, scRNA analysis) +dev = ["pytest", "ruff", "ipykernel"] +gpu = ["torch>=2.0", "torchvision>=0.15"] # for CIFAR softmax experiment + +[tool.setuptools.packages.find] +where = ["."] +include = ["src*"] + +[tool.ruff] +line-length = 100 +select = ["E", "F", "I"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/rebuild/affectivetext/README.md b/rebuild/affectivetext/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6abd0ead7efe25449120f4774990279b71a90178 --- /dev/null +++ b/rebuild/affectivetext/README.md @@ -0,0 +1,3 @@ +# affectivetext + +Rebuild gold labels from the SemEval archive and use the open fallback cache-builder when API access is unavailable. diff --git a/rebuild/affectivetext/cache_affective_text_open_predictions.py b/rebuild/affectivetext/cache_affective_text_open_predictions.py new file mode 100644 index 0000000000000000000000000000000000000000..f308f2e4171c8841fb9fe2fc771f19d23fdd0eed --- /dev/null +++ b/rebuild/affectivetext/cache_affective_text_open_predictions.py @@ -0,0 +1,179 @@ +"""Build a fully open AffectiveText prediction cache with out-of-fold regressors.""" +from __future__ import annotations + +import argparse +import json +import logging +from pathlib import Path + +import numpy as np +from sklearn.decomposition import TruncatedSVD +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.model_selection import KFold +from sklearn.neighbors import KNeighborsRegressor +from sklearn.preprocessing import Normalizer + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.data import EMOTION_NAMES, load_affective_text + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + + +def macro_pearson(a: np.ndarray, b: np.ndarray) -> float: + vals = [] + for j in range(a.shape[1]): + aj = a[:, j] + bj = b[:, j] + if np.std(aj) <= 1e-12 or np.std(bj) <= 1e-12: + continue + vals.append(float(np.corrcoef(aj, bj)[0, 1])) + return float(np.mean(vals)) if vals else float("nan") + + +def fit_predict_fold( + train_texts: list[str], + test_texts: list[str], + train_targets: np.ndarray, + n_components: int, + n_neighbors: int, +) -> np.ndarray: + vectorizer = TfidfVectorizer( + lowercase=True, + strip_accents="unicode", + sublinear_tf=True, + ngram_range=(1, 2), + min_df=1, + max_df=0.95, + stop_words="english", + ) + x_train = vectorizer.fit_transform(train_texts) + x_test = vectorizer.transform(test_texts) + + max_rank = min(x_train.shape[0] - 1, x_train.shape[1] - 1) + if max_rank >= 2: + rank = min(n_components, max_rank) + svd = TruncatedSVD(n_components=rank, random_state=0) + normalizer = Normalizer(copy=False) + x_train = normalizer.fit_transform(svd.fit_transform(x_train)) + x_test = normalizer.transform(svd.transform(x_test)) + else: + x_train = x_train.toarray() + x_test = x_test.toarray() + + knn = KNeighborsRegressor( + n_neighbors=min(n_neighbors, len(train_texts)), + weights="distance", + metric="minkowski", + p=2, + ) + knn.fit(x_train, train_targets) + return np.asarray(knn.predict(x_test), dtype=float) + + +def build_open_predictions( + headlines: list[str], + raw_scores: np.ndarray, + n_splits: int, + n_components: int, + n_neighbors: int, + seed: int, +) -> tuple[np.ndarray, np.ndarray]: + n = len(headlines) + preds = np.zeros_like(raw_scores, dtype=float) + folds = np.full(n, -1, dtype=int) + splitter = KFold(n_splits=n_splits, shuffle=True, random_state=seed) + global_mean = raw_scores.mean(axis=0) + + for fold_id, (train_idx, test_idx) in enumerate(splitter.split(headlines)): + train_texts = [headlines[i] for i in train_idx] + test_texts = [headlines[i] for i in test_idx] + train_targets = raw_scores[train_idx] + fold_preds = fit_predict_fold( + train_texts=train_texts, + test_texts=test_texts, + train_targets=train_targets, + n_components=n_components, + n_neighbors=n_neighbors, + ) + fold_preds = np.clip(fold_preds, 0.0, None) + zero_rows = fold_preds.sum(axis=1) <= 1e-12 + if np.any(zero_rows): + fold_preds[zero_rows] = global_mean + preds[test_idx] = fold_preds + folds[test_idx] = fold_id + log.info("Finished fold %d/%d", fold_id + 1, n_splits) + + return preds, folds + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--data-dir", default="data/raw/AffectiveText.Semeval.2007") + parser.add_argument("--output", default="data/processed/affective_text_open_oof_predictions.jsonl") + parser.add_argument("--n-splits", type=int, default=5) + parser.add_argument("--n-components", type=int, default=128) + parser.add_argument("--n-neighbors", type=int, default=25) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--limit", type=int, default=None) + parser.add_argument("--overwrite", action="store_true") + args = parser.parse_args() + + output_path = Path(args.output) + if output_path.exists() and not args.overwrite: + raise FileExistsError(f"Output already exists: {output_path}") + + data = load_affective_text(args.data_dir) + ids = data["ids"] + headlines = data["headlines"] + raw_scores = np.asarray(data["raw_scores"], dtype=float) + if args.limit is not None: + ids = ids[:args.limit] + headlines = headlines[:args.limit] + raw_scores = raw_scores[:args.limit] + + pred_scores, folds = build_open_predictions( + headlines=headlines, + raw_scores=raw_scores, + n_splits=args.n_splits, + n_components=args.n_components, + n_neighbors=args.n_neighbors, + seed=args.seed, + ) + + macro_r = macro_pearson(raw_scores, pred_scores) + flat_r = float(np.corrcoef(raw_scores.reshape(-1), pred_scores.reshape(-1))[0, 1]) + log.info( + "Open fallback predictor quality: macro Pearson=%.3f, flattened Pearson=%.3f", + macro_r, + flat_r, + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + for idx, headline, scores, fold_id in zip(ids, headlines, pred_scores, folds): + row = { + "id": idx, + "headline": headline, + "emotions": EMOTION_NAMES, + "scores": [float(x) for x in scores], + "provider": "open_fallback", + "model": "tfidf_svd_knn_oof", + "fold": int(fold_id), + "builder": { + "n_splits": int(args.n_splits), + "n_components": int(args.n_components), + "n_neighbors": int(args.n_neighbors), + "seed": int(args.seed), + }, + "notes": "Deterministic out-of-fold TF-IDF+SVD+kNN regression fallback.", + } + f.write(json.dumps(row, ensure_ascii=True) + "\n") + + log.info("Finished. Predictions cached at %s", output_path) + + +if __name__ == "__main__": + main() diff --git a/rebuild/affectivetext/rebuild_gold_labels.py b/rebuild/affectivetext/rebuild_gold_labels.py new file mode 100644 index 0000000000000000000000000000000000000000..4a03b8cfe2c4e80c5e3d362731313d27370c8fb6 --- /dev/null +++ b/rebuild/affectivetext/rebuild_gold_labels.py @@ -0,0 +1,6 @@ +from src.data import load_affective_text +import sys +from pathlib import Path +root = Path(sys.argv[1]) +data = load_affective_text(root) +print(data['Y'].shape) diff --git a/rebuild/affectivetext/validate_cache_schema.py b/rebuild/affectivetext/validate_cache_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..3021285af2f10b0b02816c0f56886b82341ea5e4 --- /dev/null +++ b/rebuild/affectivetext/validate_cache_schema.py @@ -0,0 +1,11 @@ +from pathlib import Path +import json +import sys +p = Path(sys.argv[1]) +with open(p) as f: + for i, line in enumerate(f, 1): + row = json.loads(line) + for field in ['id', 'scores', 'provider', 'prompt_template']: + if field not in row: + raise SystemExit(f'missing {field} at line {i}') +print('cache schema ok') diff --git a/rebuild/cifar10/README.md b/rebuild/cifar10/README.md new file mode 100644 index 0000000000000000000000000000000000000000..50d57c3457cf95049682a926c9677c75f75f53de --- /dev/null +++ b/rebuild/cifar10/README.md @@ -0,0 +1,3 @@ +# cifar10 + +Use the frozen CIFAR-10 softmax cache when available. If it is absent, regenerate the softmax predictions locally before exporting Y/U arrays. diff --git a/rebuild/cifar10/rebuild_from_torchvision.py b/rebuild/cifar10/rebuild_from_torchvision.py new file mode 100644 index 0000000000000000000000000000000000000000..744e6a926337d337fc34a8096185efc6941f5192 --- /dev/null +++ b/rebuild/cifar10/rebuild_from_torchvision.py @@ -0,0 +1,3 @@ +# Placeholder wrapper generated for the upload bundle. +# Use the main scripts/ runners in this repository together with the +# task-specific README in the same directory. diff --git a/rebuild/pbmc/README.md b/rebuild/pbmc/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8cf11239d7c71116a353dd9ee75f2212d400f5d7 --- /dev/null +++ b/rebuild/pbmc/README.md @@ -0,0 +1,3 @@ +# pbmc + +Rebuild from PBMC3K, generate pseudobulk mixtures, and then freeze the deconvolution outputs. diff --git a/rebuild/pbmc/generate_pseudobulk.py b/rebuild/pbmc/generate_pseudobulk.py new file mode 100644 index 0000000000000000000000000000000000000000..744e6a926337d337fc34a8096185efc6941f5192 --- /dev/null +++ b/rebuild/pbmc/generate_pseudobulk.py @@ -0,0 +1,3 @@ +# Placeholder wrapper generated for the upload bundle. +# Use the main scripts/ runners in this repository together with the +# task-specific README in the same directory. diff --git a/rebuild/pbmc/rebuild_from_pbmc3k.py b/rebuild/pbmc/rebuild_from_pbmc3k.py new file mode 100644 index 0000000000000000000000000000000000000000..744e6a926337d337fc34a8096185efc6941f5192 --- /dev/null +++ b/rebuild/pbmc/rebuild_from_pbmc3k.py @@ -0,0 +1,3 @@ +# Placeholder wrapper generated for the upload bundle. +# Use the main scripts/ runners in this repository together with the +# task-specific README in the same directory. diff --git a/rebuild/samson/README.md b/rebuild/samson/README.md new file mode 100644 index 0000000000000000000000000000000000000000..024d3ab43439d5e33e9d5aaf867b8608a6d4bf3f --- /dev/null +++ b/rebuild/samson/README.md @@ -0,0 +1,3 @@ +# samson + +Rebuild from the public Samson benchmark bundle and freeze the NMF abundance outputs before conformal evaluation. diff --git a/rebuild/samson/rebuild_from_public_bundle.py b/rebuild/samson/rebuild_from_public_bundle.py new file mode 100644 index 0000000000000000000000000000000000000000..744e6a926337d337fc34a8096185efc6941f5192 --- /dev/null +++ b/rebuild/samson/rebuild_from_public_bundle.py @@ -0,0 +1,3 @@ +# Placeholder wrapper generated for the upload bundle. +# Use the main scripts/ runners in this repository together with the +# task-specific README in the same directory. diff --git a/rebuild/topics/README.md b/rebuild/topics/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d05462103dd9767b88e0b631350bf2d371d0f3dc --- /dev/null +++ b/rebuild/topics/README.md @@ -0,0 +1,3 @@ +# topics + +Rebuild the topic-mixture task from the public 20 Newsgroups fetcher, then freeze the derived Y/U arrays before running the benchmark. diff --git a/rebuild/topics/rebuild_from_sklearn_fetcher.py b/rebuild/topics/rebuild_from_sklearn_fetcher.py new file mode 100644 index 0000000000000000000000000000000000000000..744e6a926337d337fc34a8096185efc6941f5192 --- /dev/null +++ b/rebuild/topics/rebuild_from_sklearn_fetcher.py @@ -0,0 +1,3 @@ +# Placeholder wrapper generated for the upload bundle. +# Use the main scripts/ runners in this repository together with the +# task-specific README in the same directory. diff --git a/rebuild/utkface/README.md b/rebuild/utkface/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0d634eafc01b6565e2212cb2e01808df56d5f011 --- /dev/null +++ b/rebuild/utkface/README.md @@ -0,0 +1,3 @@ +# utkface + +Rebuild derived age-distribution features from UTKFace locally; do not mirror the raw face-image archive. diff --git a/rebuild/utkface/rebuild_from_utkface.py b/rebuild/utkface/rebuild_from_utkface.py new file mode 100644 index 0000000000000000000000000000000000000000..744e6a926337d337fc34a8096185efc6941f5192 --- /dev/null +++ b/rebuild/utkface/rebuild_from_utkface.py @@ -0,0 +1,3 @@ +# Placeholder wrapper generated for the upload bundle. +# Use the main scripts/ runners in this repository together with the +# task-specific README in the same directory. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..c38f32602e2af0f95308761ee0e15c2888f68c4b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +numpy>=1.24 +scipy>=1.10 +scikit-learn>=1.3 +matplotlib>=3.7 +pyyaml>=6.0 +scanpy +anndata +rpy2 diff --git a/scripts/build_simplextasks_docs.py b/scripts/build_simplextasks_docs.py new file mode 100644 index 0000000000000000000000000000000000000000..d73f3f871ba221239228af0d18c25cc701131b2b --- /dev/null +++ b/scripts/build_simplextasks_docs.py @@ -0,0 +1,297 @@ +"""Build task cards and benchmark docs for the SimplexTasks-12 release.""" + +from __future__ import annotations + +import json +from pathlib import Path +from textwrap import dedent + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +RELEASE_ROOT = REPO_ROOT / "release" / "simplextasks-12" +DOCS_DIR = RELEASE_ROOT / "docs" + +REAL_EXTRAS = { + "cifar10_softmax": { + "evaluation_role": "Extreme classification-style stress test for allocation failure under fixed class-probability vectors.", + "target_definition": "A 10-way one-hot class distribution paired with a frozen ResNet-18 softmax cache.", + "default_score": "Total variation / L1 on the simplex.", + "default_stratification": "Entropy bins of the softmax prediction.", + "limitations": "This is a classification proxy rather than a naturally continuous composition task. The release ships derived arrays only and does not mirror raw CIFAR-10 images.", + }, + "topics_20ng": { + "evaluation_role": "Smooth-heterogeneity real task for topic-mixture prediction.", + "target_definition": "Fixed 10-topic compositions constructed once from the 20 Newsgroups corpus, with a TF-IDF-to-topic-mixture kNN regressor as the predictor.", + "default_score": "Aitchison distance.", + "default_stratification": "Entropy bins of the predicted topic mixture.", + "limitations": "Both the target and the predictor output are model-derived topic mixtures. The release exposes derived simplex arrays, not the raw text corpus.", + }, + "samson_unmixing": { + "evaluation_role": "Natural low-dimensional compositional benchmark aligned with grouped repair.", + "target_definition": "Three-endmember abundance compositions paired with a frozen NMF abundance estimator.", + "default_score": "Aitchison distance.", + "default_stratification": "Boundary bins on the abundance prediction.", + "limitations": "The benchmark files are public and source-cited, but the upstream bundle did not include an explicit license file; downstream reuse should preserve attribution and the original benchmark context.", + }, + "pbmc3k_pseudobulk": { + "evaluation_role": "Semi-synthetic control with known composition targets.", + "target_definition": "Pseudobulk cell-type fractions generated from PBMC3K together with a fixed NNLS deconvolution predictor.", + "default_score": "Aitchison distance.", + "default_stratification": "Boundary bins on the predicted cell-type fractions.", + "limitations": "This is a semi-synthetic control rather than a deployment-ready deconvolution benchmark. The grouped-repair story is sensitive to the pseudobulk concentration setting, which is documented in the paper appendix.", + }, + "utkface_age_ldl": { + "evaluation_role": "Image-based label-distribution task with strong grouped heterogeneity.", + "target_definition": "A 10-bin age label distribution constructed from UTKFace ages and predicted from thumbnail image features using PCA+kNN regression.", + "default_score": "Aitchison distance.", + "default_stratification": "Entropy bins of the predicted age distribution.", + "limitations": "The underlying face images are not redistributed and remain subject to the source dataset's research-use constraints. The benchmark ships derived features and simplex arrays only.", + }, + "affectivetext_emotions": { + "evaluation_role": "Small-sample weak-structure counterexample with a frozen language-model predictor.", + "target_definition": "Normalized six-emotion gold scores from SemEval-2007 AffectiveText paired with a frozen zero-shot scorer cache.", + "default_score": "Aitchison distance.", + "default_stratification": "Boundary bins on the predicted emotion mixture.", + "limitations": "The reported benchmark tables use a frozen closed-model cache; the release omits raw headlines and raw API responses. A fully open TF-IDF+SVD+kNN fallback cache-builder is included for local reproduction, but it is not identical to the main predictor used in the paper tables.", + }, +} + +SYNTH_EXTRAS = { + "d1_homogeneous": { + "evaluation_role": "Negative control with no residual-scale heterogeneity.", + "limitations": "Intended to confirm that the benchmark does not manufacture allocation failures when the DGP is homogeneous.", + }, + "d2_pure_scale": { + "evaluation_role": "Canonical smooth-scale heterogeneity regime used to motivate normalization-based repair.", + "limitations": "This regime isolates scale variation only; it is not intended to cover bias-type misspecification or changing tail shape.", + }, + "d3_discrete_groups_aligned": { + "evaluation_role": "Aligned discrete-group regime where group-wise calibration should have an inductive advantage.", + "limitations": "The grouping is built into the DGP. This is a positive case for partition-aligned repair, not evidence that arbitrary grouped calibration is always stable.", + }, + "d4_model_bias": { + "evaluation_role": "Bias-type counterexample where changing the wrapper alone is not enough.", + "limitations": "This regime is not reducible to a single local scale field, so normalization cannot be expected to fully repair it.", + }, + "d5_heavy_tail": { + "evaluation_role": "Smooth-scale regime with heavy tails to stress robustness beyond Gaussian residuals.", + "limitations": "The regime is still synthetic and targeted; it does not span every possible deviation from the baseline score distribution.", + }, + "d6_high_k": { + "evaluation_role": "High-dimensional simplex regime exposing the compute and allocation challenges that appear when K is large.", + "limitations": "Benchmark-scale exact methods are too expensive here; the paper marks the exact-reference entries separately as D6 dagger cells.", + }, +} + + +def load_json(path: Path) -> dict: + with open(path) as f: + return json.load(f) + + +def load_yaml(path: Path) -> dict: + with open(path) as f: + return yaml.safe_load(f) + + +def write(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text.rstrip() + "\n") + + +def bullet_list(items: list[str]) -> str: + return "\n".join(f"- {item}" for item in items) + + +def synthetic_task_card(task_dir: Path, metadata: dict, config: dict, extra: dict) -> str: + evaluation = config["evaluation"] + dgp = config["dgp"] + data = config["data"] + lines = [ + f"# {metadata['task_name']} Task Card", + "", + f"- Task ID: `{metadata['task_id']}`", + "- Subset: synthetic", + f"- Samples: `{metadata['n_samples']}`", + f"- Simplex dimension: `{metadata['simplex_dim']}`", + f"- Predictor: {metadata['predictor']}", + f"- Regime label: {metadata['regime_label']}", + "", + "## Evaluation Role", + "", + extra["evaluation_role"], + "", + "## DGP Summary", + "", + f"- DGP family: `{dgp['name']}`", + "- Default score: Aitchison distance", + f"- Default stratification: `{evaluation['strata_method']}` with `{evaluation['n_strata']}` strata", + f"- Calibration size: `{data['n_cal']}`", + f"- Test size: `{data['n_test']}`", + f"- Repetitions: `{data['n_rep']}`", + "", + "## Release Contents", + "", + bullet_list([f"`{name}`" for name in metadata["available_arrays"]]), + "", + "## Provenance And Rebuild", + "", + f"- Source asset: {metadata['source_asset']}", + f"- Config file: `{metadata['config_file']}`", + f"- Redistribution: `{metadata['redistribution']}`", + f"- Seed: `{metadata['seed']}`", + "", + "## Limitations", + "", + extra["limitations"], + ] + return "\n".join(lines) + + +def real_task_card(task_dir: Path, metadata: dict, extra: dict) -> str: + lines = [ + f"# {metadata['task_name']} Task Card", + "", + f"- Task ID: `{metadata['task_id']}`", + "- Subset: real", + f"- Samples: `{metadata['n_samples']}`", + f"- Simplex dimension: `{metadata['simplex_dim']}`", + f"- Predictor: {metadata['predictor']}", + "", + "## Evaluation Role", + "", + extra["evaluation_role"], + "", + "## Target And Predictor", + "", + extra["target_definition"], + "", + "## Default Benchmark Settings", + "", + f"- Default score: {extra['default_score']}", + f"- Default stratification: {extra['default_stratification']}", + f"- Redistribution: `{metadata['redistribution']}`", + "", + "## Release Contents", + "", + bullet_list([f"`{name}`" for name in metadata["available_arrays"]]), + "", + "## Provenance And Usage Notes", + "", + f"- Source asset: {metadata['source_asset']}", + f"- Metadata note: {metadata['notes']}", + "", + "## Limitations", + "", + extra["limitations"], + ] + return "\n".join(lines) + + +def build_task_cards() -> None: + for task_dir in sorted((RELEASE_ROOT / "real").glob("*")): + metadata = load_json(task_dir / "metadata.json") + extra = REAL_EXTRAS[metadata["task_id"]] + write(task_dir / "task_card.md", real_task_card(task_dir, metadata, extra)) + + for task_dir in sorted((RELEASE_ROOT / "synthetic").glob("*")): + metadata = load_json(task_dir / "metadata.json") + config = load_yaml(task_dir / "config.yaml") + extra = SYNTH_EXTRAS[metadata["task_id"]] + write(task_dir / "task_card.md", synthetic_task_card(task_dir, metadata, config, extra)) + + +def build_docs() -> None: + benchmark_card = dedent( + """ + # SimplexTasks-12 Benchmark Card + + SimplexTasks-12 is the processed task collection behind the SimplexUQ benchmark. It is designed to support evaluation claims about coverage allocation for simplex-valued uncertainty quantification under fixed predictors and fixed nonconformity scores. + + ## Supported Claims + + - Whether a fixed simplex-valued predictor exhibits allocation failure beyond its marginal coverage. + - Which heterogeneity regime best describes the observed failure pattern. + - Which conformal wrapper family is most competitive under the chosen task and stratification protocol. + + ## Claims The Benchmark Does Not Support + + - Universal wrapper rankings across all simplex tasks. + - Deployment-readiness claims for any predictor. + - Raw-data redistribution rights beyond the terms documented in `LICENSE_NOTES.md`. + + ## Benchmark Contents + + - 6 synthetic regimes with canonical `task.npz` bundles and copied YAML configs. + - 6 fixed-predictor real tasks with cached `(Y, U)` arrays or derived features. + - Per-task `task_card.md` files and `metadata.json` provenance records. + - Release-level rebuild instructions for the paper tables and figures. + + ## Reproducibility Contract + + - Benchmark evaluation always operates on frozen predictor outputs. + - Default stratification rules are fixed before wrapper comparison. + - Restricted raw assets are replaced by derived arrays plus rebuild notes. + - The paper figures and tables can be regenerated from cached outputs with the commands in the release `README.md`. + + ## Responsible Use + + Some tasks rely on non-redistributable source assets or derived features from sensitive domains such as face images. The release therefore packages evaluation-ready derived arrays rather than raw mirrors, and downstream users should preserve the source citations and usage notes attached to each task card. + """ + ).strip() + + evaluation_protocol = dedent( + """ + # SimplexTasks-12 Evaluation Protocol + + ## Fixed-Predictor Principle + + Every benchmark slice fixes the predictor first and then varies only the conformal wrapper. This keeps the evaluation target on uncertainty allocation rather than on predictor training. + + ## Default Scores + + - Real and synthetic composition tasks use Aitchison distance unless the target lies on simplex vertices. + - CIFAR-10 uses total variation / L1 because Aitchison distance is ill-defined at the one-hot boundary. + + ## Stratification Rules + + - Default strata are entropy bins, boundary bins, or task-specific dominant-group partitions. + - Sensitivity sweeps use fixed alternative stratifications defined from cached prediction vectors only. + - Stratification maps are not tuned per wrapper and do not depend on calibration/test responses. + + ## Main Metrics + + - Marginal coverage. + - Max disparity across prediction-space strata. + - Worst-stratum coverage. + - Coverage variance. + - SSCV and mean radius; low-dimensional synthetic tasks also report a simplex-volume ratio. + + ## Wrapper Families + + - Global split conformal. + - Group-wise / Mondrian conformal. + - Two-stage normalization. + - Exact or leave-one-out references where affordable. + - Diagnostic variants such as OneShot, TrainRes, and the current weighted implementation. + + ## Output Interpretation + + The benchmark is designed to compare wrapper families under visible allocation-efficiency-compute tradeoffs. It should not be reduced to a single leaderboard or read as a conditional-coverage certification protocol. + """ + ).strip() + + write(DOCS_DIR / "benchmark_card.md", benchmark_card) + write(DOCS_DIR / "evaluation_protocol.md", evaluation_protocol) + + +def main() -> None: + build_task_cards() + build_docs() + print("Built SimplexTasks-12 task cards and docs.") + + +if __name__ == "__main__": + main() diff --git a/scripts/cache_affective_text_open_predictions.py b/scripts/cache_affective_text_open_predictions.py new file mode 100644 index 0000000000000000000000000000000000000000..f308f2e4171c8841fb9fe2fc771f19d23fdd0eed --- /dev/null +++ b/scripts/cache_affective_text_open_predictions.py @@ -0,0 +1,179 @@ +"""Build a fully open AffectiveText prediction cache with out-of-fold regressors.""" +from __future__ import annotations + +import argparse +import json +import logging +from pathlib import Path + +import numpy as np +from sklearn.decomposition import TruncatedSVD +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.model_selection import KFold +from sklearn.neighbors import KNeighborsRegressor +from sklearn.preprocessing import Normalizer + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.data import EMOTION_NAMES, load_affective_text + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + + +def macro_pearson(a: np.ndarray, b: np.ndarray) -> float: + vals = [] + for j in range(a.shape[1]): + aj = a[:, j] + bj = b[:, j] + if np.std(aj) <= 1e-12 or np.std(bj) <= 1e-12: + continue + vals.append(float(np.corrcoef(aj, bj)[0, 1])) + return float(np.mean(vals)) if vals else float("nan") + + +def fit_predict_fold( + train_texts: list[str], + test_texts: list[str], + train_targets: np.ndarray, + n_components: int, + n_neighbors: int, +) -> np.ndarray: + vectorizer = TfidfVectorizer( + lowercase=True, + strip_accents="unicode", + sublinear_tf=True, + ngram_range=(1, 2), + min_df=1, + max_df=0.95, + stop_words="english", + ) + x_train = vectorizer.fit_transform(train_texts) + x_test = vectorizer.transform(test_texts) + + max_rank = min(x_train.shape[0] - 1, x_train.shape[1] - 1) + if max_rank >= 2: + rank = min(n_components, max_rank) + svd = TruncatedSVD(n_components=rank, random_state=0) + normalizer = Normalizer(copy=False) + x_train = normalizer.fit_transform(svd.fit_transform(x_train)) + x_test = normalizer.transform(svd.transform(x_test)) + else: + x_train = x_train.toarray() + x_test = x_test.toarray() + + knn = KNeighborsRegressor( + n_neighbors=min(n_neighbors, len(train_texts)), + weights="distance", + metric="minkowski", + p=2, + ) + knn.fit(x_train, train_targets) + return np.asarray(knn.predict(x_test), dtype=float) + + +def build_open_predictions( + headlines: list[str], + raw_scores: np.ndarray, + n_splits: int, + n_components: int, + n_neighbors: int, + seed: int, +) -> tuple[np.ndarray, np.ndarray]: + n = len(headlines) + preds = np.zeros_like(raw_scores, dtype=float) + folds = np.full(n, -1, dtype=int) + splitter = KFold(n_splits=n_splits, shuffle=True, random_state=seed) + global_mean = raw_scores.mean(axis=0) + + for fold_id, (train_idx, test_idx) in enumerate(splitter.split(headlines)): + train_texts = [headlines[i] for i in train_idx] + test_texts = [headlines[i] for i in test_idx] + train_targets = raw_scores[train_idx] + fold_preds = fit_predict_fold( + train_texts=train_texts, + test_texts=test_texts, + train_targets=train_targets, + n_components=n_components, + n_neighbors=n_neighbors, + ) + fold_preds = np.clip(fold_preds, 0.0, None) + zero_rows = fold_preds.sum(axis=1) <= 1e-12 + if np.any(zero_rows): + fold_preds[zero_rows] = global_mean + preds[test_idx] = fold_preds + folds[test_idx] = fold_id + log.info("Finished fold %d/%d", fold_id + 1, n_splits) + + return preds, folds + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--data-dir", default="data/raw/AffectiveText.Semeval.2007") + parser.add_argument("--output", default="data/processed/affective_text_open_oof_predictions.jsonl") + parser.add_argument("--n-splits", type=int, default=5) + parser.add_argument("--n-components", type=int, default=128) + parser.add_argument("--n-neighbors", type=int, default=25) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--limit", type=int, default=None) + parser.add_argument("--overwrite", action="store_true") + args = parser.parse_args() + + output_path = Path(args.output) + if output_path.exists() and not args.overwrite: + raise FileExistsError(f"Output already exists: {output_path}") + + data = load_affective_text(args.data_dir) + ids = data["ids"] + headlines = data["headlines"] + raw_scores = np.asarray(data["raw_scores"], dtype=float) + if args.limit is not None: + ids = ids[:args.limit] + headlines = headlines[:args.limit] + raw_scores = raw_scores[:args.limit] + + pred_scores, folds = build_open_predictions( + headlines=headlines, + raw_scores=raw_scores, + n_splits=args.n_splits, + n_components=args.n_components, + n_neighbors=args.n_neighbors, + seed=args.seed, + ) + + macro_r = macro_pearson(raw_scores, pred_scores) + flat_r = float(np.corrcoef(raw_scores.reshape(-1), pred_scores.reshape(-1))[0, 1]) + log.info( + "Open fallback predictor quality: macro Pearson=%.3f, flattened Pearson=%.3f", + macro_r, + flat_r, + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + for idx, headline, scores, fold_id in zip(ids, headlines, pred_scores, folds): + row = { + "id": idx, + "headline": headline, + "emotions": EMOTION_NAMES, + "scores": [float(x) for x in scores], + "provider": "open_fallback", + "model": "tfidf_svd_knn_oof", + "fold": int(fold_id), + "builder": { + "n_splits": int(args.n_splits), + "n_components": int(args.n_components), + "n_neighbors": int(args.n_neighbors), + "seed": int(args.seed), + }, + "notes": "Deterministic out-of-fold TF-IDF+SVD+kNN regression fallback.", + } + f.write(json.dumps(row, ensure_ascii=True) + "\n") + + log.info("Finished. Predictions cached at %s", output_path) + + +if __name__ == "__main__": + main() diff --git a/scripts/cache_affective_text_predictions.py b/scripts/cache_affective_text_predictions.py new file mode 100644 index 0000000000000000000000000000000000000000..c1b7ef930cfdbd9578ed024857dd462cd82d15c4 --- /dev/null +++ b/scripts/cache_affective_text_predictions.py @@ -0,0 +1,209 @@ +"""Cache zero-shot API emotion scores for SemEval-2007 Affective Text.""" +from __future__ import annotations + +import argparse +import json +import logging +import os +import re +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.data import EMOTION_NAMES, load_affective_text, load_prediction_cache + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +PROMPT_TEMPLATE = ( + 'Rate the following news headline on 6 emotions: anger, disgust, fear, joy, sadness, surprise. ' + 'Return only 6 numbers from 0 to 100, comma-separated, in that order.\n' + 'Headline: "{headline}"\n' + "Scores:" +) + + +def parse_scores(text: str) -> list[float]: + nums = re.findall(r"-?\d+(?:\.\d+)?", text) + if len(nums) < 6: + raise ValueError(f"Could not parse 6 scores from response: {text!r}") + scores = [max(float(x), 0.0) for x in nums[:6]] + if sum(scores) <= 0: + raise ValueError(f"Parsed zero-sum scores from response: {text!r}") + return scores + + +def call_openai_chat_completions( + headline: str, + model: str, + api_key: str, + base_url: str, + timeout_sec: float, +) -> tuple[str, dict]: + prompt = PROMPT_TEMPLATE.format(headline=headline) + payload = { + "model": model, + "messages": [ + {"role": "system", "content": "You are a precise annotation model."}, + {"role": "user", "content": prompt}, + ], + "temperature": 0, + } + req = urllib.request.Request( + url=base_url.rstrip("/") + "/chat/completions", + data=json.dumps(payload).encode("utf-8"), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + }, + method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout_sec) as resp: + body = json.loads(resp.read().decode("utf-8")) + text = body["choices"][0]["message"]["content"] + return text, body + + +def call_gemini_generate_content( + headline: str, + model: str, + api_key: str, + base_url: str, + timeout_sec: float, +) -> tuple[str, dict]: + prompt = PROMPT_TEMPLATE.format(headline=headline) + payload = { + "contents": [ + { + "role": "user", + "parts": [{"text": prompt}], + } + ], + "generationConfig": { + "temperature": 0, + }, + } + url = ( + base_url.rstrip("/") + + f"/models/{model}:generateContent?key={urllib.parse.quote(api_key)}" + ) + req = urllib.request.Request( + url=url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout_sec) as resp: + body = json.loads(resp.read().decode("utf-8")) + candidates = body.get("candidates", []) + if not candidates: + raise KeyError(f"No Gemini candidates in response: {body}") + parts = candidates[0].get("content", {}).get("parts", []) + text = "\n".join(part.get("text", "") for part in parts if part.get("text")) + if not text: + raise KeyError(f"No text parts in Gemini response: {body}") + return text, body + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--data-dir", default="data/raw/AffectiveText.Semeval.2007") + parser.add_argument("--output", default="data/processed/affective_text_predictions.jsonl") + parser.add_argument("--provider", choices=["openai", "gemini"], default="gemini") + parser.add_argument("--model", default=None) + parser.add_argument("--base-url", default=None) + parser.add_argument("--api-key-env", default=None) + parser.add_argument("--limit", type=int, default=None) + parser.add_argument("--sleep-sec", type=float, default=0.0) + parser.add_argument("--timeout-sec", type=float, default=60.0) + parser.add_argument("--overwrite", action="store_true") + args = parser.parse_args() + + if args.model is None: + if args.provider == "gemini": + args.model = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash-001") + else: + args.model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini-2024-07-18") + if args.base_url is None: + if args.provider == "gemini": + args.base_url = os.environ.get("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta") + else: + args.base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") + if args.api_key_env is None: + args.api_key_env = "GEMINI_API_KEY" if args.provider == "gemini" else "OPENAI_API_KEY" + + api_key = os.environ.get(args.api_key_env) + if not api_key: + raise EnvironmentError(f"Missing API key in env var {args.api_key_env}") + + data = load_affective_text(args.data_dir) + ids = data["ids"] + headlines = data["headlines"] + if args.limit is not None: + ids = ids[:args.limit] + headlines = headlines[:args.limit] + + out_path = Path(args.output) + out_path.parent.mkdir(parents=True, exist_ok=True) + existing = {} + if out_path.exists() and not args.overwrite: + existing = load_prediction_cache(out_path) + log.info(f"Loaded {len(existing)} cached predictions from {out_path}") + + n_done = 0 + with open(out_path, "a" if existing and not args.overwrite else "w", encoding="utf-8") as f: + for idx, headline in zip(ids, headlines): + if idx in existing and not args.overwrite: + continue + try: + if args.provider == "gemini": + raw_text, raw_json = call_gemini_generate_content( + headline=headline, + model=args.model, + api_key=api_key, + base_url=args.base_url, + timeout_sec=args.timeout_sec, + ) + else: + raw_text, raw_json = call_openai_chat_completions( + headline=headline, + model=args.model, + api_key=api_key, + base_url=args.base_url, + timeout_sec=args.timeout_sec, + ) + scores = parse_scores(raw_text) + except (urllib.error.URLError, urllib.error.HTTPError, ValueError, KeyError) as exc: + log.error(f"Failed on id={idx}: {exc}") + continue + + row = { + "id": idx, + "headline": headline, + "emotions": EMOTION_NAMES, + "scores": scores, + "provider": args.provider, + "model": args.model, + "base_url": args.base_url, + "prompt_template": PROMPT_TEMPLATE, + "raw_text": raw_text, + "raw_response": raw_json, + } + f.write(json.dumps(row, ensure_ascii=True) + "\n") + f.flush() + n_done += 1 + if n_done % 50 == 0: + log.info(f"Cached {n_done} new predictions") + if args.sleep_sec > 0: + time.sleep(args.sleep_sec) + + log.info(f"Finished. Predictions cached at {out_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_artifact_integrity.py b/scripts/check_artifact_integrity.py new file mode 100644 index 0000000000000000000000000000000000000000..ae176abe09b7a30cdafc44a08badc01fab98465d --- /dev/null +++ b/scripts/check_artifact_integrity.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT.parent / 'SimplexTasks-12-data' + +REQUIRED_DATA_FILES = [ + 'README.md', + 'CITATION.cff', + 'LICENSE', + 'MANIFEST.json', + 'BENCHMARK_PROTOCOL.md', + 'EVALUATION_PROTOCOL.md', + 'ASSET_PROVENANCE_AND_TERMS.md', + 'REPRODUCIBILITY.md', + 'data/viewer/tasks.jsonl', +] +REQUIRED_REAL_DIRS = [ + 'cifar10_softmax', + 'topics_20newsgroups', + 'samson_unmixing', + 'pbmc_pseudobulk', + 'utkface_ldl', + 'affectivetext', +] +REQUIRED_SYNTH_DIRS = ['D1', 'D2', 'D3', 'D4', 'D5', 'D6'] +REQUIRED_CODE_FILES = [ + 'README.md', + 'CITATION.cff', + 'LICENSE', + 'environment.yml', + 'requirements.txt', + 'scripts/reproduce_tables.py', + 'scripts/reproduce_figures.py', + 'scripts/run_benchmark.py', + 'docs/reviewer_quickstart.md', +] + + +def require(path: Path, label: str, issues: list[str]) -> None: + if not path.exists(): + issues.append(f'missing {label}: {path}') + + +def main() -> None: + issues: list[str] = [] + require(DATA, 'dataset bundle', issues) + for rel in REQUIRED_CODE_FILES: + require(ROOT / rel, f'code file {rel}', issues) + if DATA.exists(): + for rel in REQUIRED_DATA_FILES: + require(DATA / rel, f'data file {rel}', issues) + for name in REQUIRED_REAL_DIRS: + require(DATA / 'data' / 'derived' / name, f'derived task {name}', issues) + for name in REQUIRED_SYNTH_DIRS: + require(DATA / 'data' / 'synthetic' / name, f'synthetic task {name}', issues) + require(DATA / 'data' / 'summaries' / 'figure_inputs', 'figure input cache', issues) + require(DATA / 'data' / 'summaries' / 'table_inputs', 'table input cache', issues) + viewer_path = DATA / 'data' / 'viewer' / 'tasks.jsonl' + if viewer_path.exists(): + rows = [json.loads(line) for line in viewer_path.read_text(encoding='utf-8').splitlines() if line.strip()] + schemas = {tuple(row.keys()) for row in rows} + if len(rows) != 12: + issues.append(f'unexpected row count in data/viewer/tasks.jsonl: {len(rows)}') + if len(schemas) != 1: + issues.append('inconsistent JSONL schema in data/viewer/tasks.jsonl') + manifest_path = DATA / 'MANIFEST.json' + if manifest_path.exists(): + manifest = json.loads(manifest_path.read_text(encoding='utf-8')) + if manifest.get('task_count') != 12: + issues.append(f"unexpected task_count in MANIFEST.json: {manifest.get('task_count')}") + if issues: + raise SystemExit('\n'.join(issues)) + print('artifact integrity ok') + + +if __name__ == '__main__': + main() diff --git a/scripts/export_summary_files.py b/scripts/export_summary_files.py new file mode 100644 index 0000000000000000000000000000000000000000..5e780b9f283168dd872de10d74a2b22c81903f04 --- /dev/null +++ b/scripts/export_summary_files.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import shutil +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT.parent / 'SimplexTasks-12-data' +OUTPUTS = ROOT / 'outputs' / 'tables' + + +def main() -> None: + if not DATA.exists(): + raise SystemExit('SimplexTasks-12-data bundle not found next to the code bundle') + OUTPUTS.mkdir(parents=True, exist_ok=True) + for src_dir_name in ['table_inputs', 'figure_inputs']: + src_dir = DATA / 'data' / 'summaries' / src_dir_name + if not src_dir.exists(): + continue + for src in src_dir.iterdir(): + if src.is_file(): + shutil.copy2(src, OUTPUTS / src.name) + + +if __name__ == '__main__': + main() diff --git a/scripts/make_appendix_tables.py b/scripts/make_appendix_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..684d5c5ca618612a83e7a9d40a761814421ec3e8 --- /dev/null +++ b/scripts/make_appendix_tables.py @@ -0,0 +1,226 @@ +"""Generate appendix-ready LaTeX tables from benchmark summaries.""" +from __future__ import annotations + +import json +from pathlib import Path + +from make_tables import EXTRA_FILES, METHOD_LABELS, METHOD_ORDER, REAL_SPECS, extract_summary, load_json + +SUMMARY_METHODS = ["global", "partition", "twostage", "fullcp", "jackknife_plus", "oneshot", "trainres", "weighted"] +PBMC_RUNTIME_FALLBACK = "pbmc_sensitivity_exp2_1_bulk_deconv_boundary_fixed.json" + + +def latex_escape(text: str) -> str: + return ( + text.replace("\\", "\\textbackslash{}") + .replace("_", "\\_") + .replace("%", "\\%") + .replace("&", "\\&") + .replace("#", "\\#") + ) + + +def merge_summary(label: str, filename: str, results_dir: Path) -> dict: + summary = extract_summary(load_json(results_dir / filename)) + if label in EXTRA_FILES: + for extra_name in EXTRA_FILES[label]: + extra_path = results_dir / extra_name + if extra_path.exists(): + summary = {**summary, **extract_summary(load_json(extra_path))} + return summary + + +def format_cov(entry: dict | None) -> str: + if not entry: + return "--" + return f"{entry['mean']:.3f}" + + +def metric_mean(entry: dict | None, key: str) -> str: + if not entry: + return "--" + value = entry.get(key) + if value is None: + return "--" + if isinstance(value, dict): + return f"{value['mean']:.3f}" + return f"{float(value):.3f}" + + +def worst_cov(entry: dict | None) -> str: + if not entry: + return "--" + if "worst_stratum_coverage" in entry: + return metric_mean(entry, "worst_stratum_coverage") + stratified = entry.get("stratified_coverage", {}) + if not stratified: + return "--" + vals = [] + for value in stratified.values(): + if isinstance(value, dict): + vals.append(float(value["mean"])) + else: + vals.append(float(value)) + return "--" if not vals else f"{min(vals):.3f}" + + +def runtime_mean(summary: dict, method: str, label: str, results_dir: Path) -> str: + entry = summary.get(method) + if entry and "runtime_sec" in entry: + return metric_mean(entry, "runtime_sec") + if label == "PBMC": + fallback_path = results_dir / PBMC_RUNTIME_FALLBACK + if fallback_path.exists(): + fallback_summary = extract_summary(load_json(fallback_path)) + fallback_entry = fallback_summary.get(method) + if fallback_entry and "runtime_sec" in fallback_entry: + return metric_mean(fallback_entry, "runtime_sec") + return "--" + + +def build_real_summary_tables(results_dir: Path) -> list[str]: + blocks: list[str] = [] + task_groups = [REAL_SPECS[:3], REAL_SPECS[3:]] + for table_idx, specs in enumerate(task_groups, start=1): + blocks.append("\\begin{table*}[!htbp]") + blocks.append("\\centering") + blocks.append( + "\\caption{Real-task summary metrics. Rows report mean marginal coverage, worst-stratum coverage, max disparity, mean radius, and runtime per repetition for each default real-data benchmark task and method. PBMC runtime entries use the corresponding boundary-based rerun because the original deconvolution summary files predated runtime logging for several split methods.}" + if table_idx == 1 else + "\\caption[]{Real-task summary metrics (continued).}" + ) + blocks.append(f"\\label{{tab:real-summary-{table_idx}}}") + blocks.append("\\scriptsize") + blocks.append("\\setlength{\\tabcolsep}{4pt}") + blocks.append("\\begin{tabular}{@{}llccccc@{}}") + blocks.append("\\toprule") + blocks.append("Task & Method & Coverage & Worst & Disparity & Radius & Runtime (s) \\\\") + blocks.append("\\midrule") + + first_task = True + for filename, label in specs: + path = results_dir / filename + if not path.exists(): + continue + summary = merge_summary(label, filename, results_dir) + methods_present = [m for m in SUMMARY_METHODS if m in summary] + if not methods_present: + continue + if not first_task: + blocks.append("\\midrule") + first_task = False + for row_idx, method in enumerate(methods_present): + entry = summary.get(method) + task_cell = ( + f"\\multirow{{{len(methods_present)}}}{{*}}{{{latex_escape(label)}}}" + if row_idx == 0 else "" + ) + row = [ + task_cell, + METHOD_LABELS[method], + metric_mean(entry, "marginal_coverage"), + worst_cov(entry), + metric_mean(entry, "max_disparity"), + metric_mean(entry, "mean_radius"), + runtime_mean(summary, method, label, results_dir), + ] + blocks.append(" & ".join(row) + " \\\\") + blocks.append("\\bottomrule") + blocks.append("\\end{tabular}") + blocks.append("\\end{table*}") + blocks.append("") + return blocks + + +def build_per_strata_tables(results_dir: Path) -> list[str]: + blocks: list[str] = [] + for filename, label in REAL_SPECS: + path = results_dir / filename + if not path.exists(): + continue + summary = merge_summary(label, filename, results_dir) + ref_method = next((m for m in SUMMARY_METHODS if m in summary), None) + if ref_method is None: + continue + strata_keys = sorted(summary[ref_method]["stratified_coverage"].keys(), key=int) + cols = "l" + "c" * len(SUMMARY_METHODS) + blocks.append("\\begin{table*}[!htbp]") + blocks.append("\\centering") + blocks.append( + f"\\caption{{Per-stratum coverage for {latex_escape(label)}. Entries report mean empirical coverage across repetitions.}}" + ) + blocks.append(f"\\label{{tab:strata-{label.lower().replace(' ', '-')}}}") + blocks.append("\\small") + blocks.append(f"\\begin{{tabular}}{{@{{}}{cols}@{{}}}}") + blocks.append("\\toprule") + headers = ["Stratum"] + [METHOD_LABELS[m] for m in SUMMARY_METHODS] + blocks.append(" & ".join(headers) + " \\\\") + blocks.append("\\midrule") + for k in strata_keys: + row = [f"S{k}"] + for method in SUMMARY_METHODS: + entry = summary.get(method, {}).get("stratified_coverage", {}).get(k) + row.append(format_cov(entry)) + blocks.append(" & ".join(row) + " \\\\") + blocks.append("\\bottomrule") + blocks.append("\\end{tabular}") + blocks.append("\\end{table*}") + blocks.append("") + return blocks + + +def build_runtime_table(results_dir: Path) -> list[str]: + blocks: list[str] = [] + blocks.append("\\begin{table*}[!htbp]") + blocks.append("\\centering") + blocks.append("\\caption{Mean runtime per repetition in seconds on the real-data benchmark. Entries marked \\texttt{--} were not run for that task.}") + blocks.append("\\label{tab:runtime-real}") + blocks.append("\\small") + cols = "l" + "c" * len(REAL_SPECS) + blocks.append(f"\\begin{{tabular}}{{@{{}}{cols}@{{}}}}") + blocks.append("\\toprule") + blocks.append("Method & " + " & ".join(label for _, label in REAL_SPECS) + " \\\\") + blocks.append("\\midrule") + for method in SUMMARY_METHODS: + row = [METHOD_LABELS[method]] + for filename, label in REAL_SPECS: + path = results_dir / filename + if not path.exists(): + row.append("--") + continue + summary = merge_summary(label, filename, results_dir) + runtime = runtime_mean(summary, method, label, results_dir) + row.append(runtime if runtime == "--" else f"{float(runtime):.2f}") + blocks.append(" & ".join(row) + " \\\\") + blocks.append("\\bottomrule") + blocks.append("\\end{tabular}") + blocks.append("\\end{table*}") + blocks.append("") + return blocks + + +def main() -> None: + results_dir = Path("results/tables") + out_path = Path("paper/rewrite_2026/latex/generated_appendix_tables.tex") + blocks: list[str] = [] + blocks.append("% Auto-generated by scripts/make_appendix_tables.py") + blocks.append("\\FloatBarrier") + blocks.append("\\subsection{Real-task summary tables}") + blocks.append("") + blocks.extend(build_real_summary_tables(results_dir)) + blocks.append("\\clearpage") + blocks.append("\\FloatBarrier") + blocks.append("\\subsection{Per-strata coverage tables}") + blocks.append("") + blocks.extend(build_per_strata_tables(results_dir)) + blocks.append("\\clearpage") + blocks.append("\\FloatBarrier") + blocks.append("\\subsection{Runtime comparison}") + blocks.append("") + blocks.extend(build_runtime_table(results_dir)) + out_path.write_text("\n".join(blocks) + "\n") + print(f"Saved {out_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/make_figures.py b/scripts/make_figures.py new file mode 100644 index 0000000000000000000000000000000000000000..cc5663b88c03bbef78cfddb2476da69f71297061 --- /dev/null +++ b/scripts/make_figures.py @@ -0,0 +1,776 @@ +"""Generate SimplexUQ benchmark figures from saved results.""" +import argparse +import json +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import matplotlib.patheffects as pe +from matplotlib.patches import Rectangle +import numpy as np + +plt.rcParams.update({ + "font.size": 9, + "font.family": "sans-serif", + "font.sans-serif": ["DejaVu Sans", "Arial"], + "axes.labelsize": 10, + "axes.titlesize": 10, + "legend.fontsize": 9, + "xtick.labelsize": 8, + "ytick.labelsize": 8, + "figure.dpi": 150, + "savefig.dpi": 300, + "savefig.bbox": "tight", + "axes.spines.top": False, + "axes.spines.right": False, + "axes.grid": False, +}) + +METHOD_LABELS = { + "global": "Global", + "partition": "Mondrian", + "twostage": "TwoStage", + "fullcp": "FullCP", + "jackknife_plus": "Jackknife+", + "weighted": "Weighted", + "oracle": "Oracle", + "oneshot": "OneShot", + "trainres": "TrainRes", +} + +METHOD_COLORS = { + "global": "#D55E00", + "partition": "#0072B2", + "twostage": "#009E73", + "fullcp": "#56B4E9", + "jackknife_plus": "#CC79A7", + "weighted": "#F0E442", + "oracle": "#000000", + "oneshot": "#7F7F7F", + "trainres": "#E69F00", +} + +METHOD_ORDER = [ + "global", + "partition", + "twostage", + "fullcp", + "jackknife_plus", + "oneshot", + "trainres", + "weighted", + "oracle", +] + +REPO_ROOT = Path(__file__).resolve().parents[1] +PAPER_FIG_DIR = REPO_ROOT / "paper" / "rewrite_2026" / "latex" / "figures" + +DGP_SPECS = [ + ("d1_homogeneous", "D1\nHom."), + ("d2_pure_scale", "D2\nScale"), + ("d3_discrete_groups_aligned", "D3\nDiscrete"), + ("d4_model_bias", "D4\nBias"), + ("d5_heavy_tail", "D5\nTail"), + ("d6_high_k", "D6$^{\\dagger}$\nHigh-K"), +] + +SYNTH_EXTRA_FILES = { + "d1_homogeneous": ["d1_homogeneous_exact.json"], + "d3_discrete_groups_aligned": ["d3_discrete_groups_aux.json"], + "d5_heavy_tail": ["d5_heavy_tail_aux.json"], + "d6_high_k": ["d6_high_k_aux.json", "d6_high_k_exact_appendix.json"], +} + +REAL_SPECS = [ + ("exp2_2_softmax_cifar10_strata_entropy_fixed.json", "CIFAR-10"), + ("exp2_3_hyperspectral_samson_nmf_all_methods.json", "Samson"), + ("exp2_5_topics_K10_all_methods.json", "Topics"), + ("exp2_6_affective_text.json", "AffectiveText"), + ("exp2_4_age_ldl_K10_image_knn_main.json", "UTKFace"), + ("real_bulk_deconv.json", "PBMC"), +] + +REAL_EXTRA_FILES = { + "PBMC": [ + "real_bulk_deconv_fullcp.json", + "real_bulk_deconv_aux.json", + "real_bulk_deconv_trainres.json", + ], + "UTKFace": ["exp2_4_age_ldl_K10_image_knn_fullcp_2k.json"], +} + +REAL_MARKERS = { + "global": "o", + "partition": "s", + "twostage": "^", + "fullcp": "D", + "jackknife_plus": "P", + "trainres": "X", +} + +PROFILE_MARKERS = { + "global": "o", + "partition": "s", + "twostage": "^", + "fullcp": "D", + "jackknife_plus": "P", + "oracle": "X", +} + + +def load_json(path: Path) -> dict: + with open(path) as f: + return json.load(f) + + +def save_figure(fig: plt.Figure, output_dir: Path, filename: str) -> None: + """Save a figure to the results directory and mirror it into the paper tree.""" + output_dir.mkdir(parents=True, exist_ok=True) + PAPER_FIG_DIR.mkdir(parents=True, exist_ok=True) + out = output_dir / filename + mirror = PAPER_FIG_DIR / filename + fig.savefig(out) + fig.savefig(mirror) + print(f"Saved {out}") + print(f"Mirrored {mirror}") + + +def simplex_to_xy(U: np.ndarray) -> np.ndarray: + """Map 3-simplex points to 2D barycentric coordinates.""" + vertices = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [0.5, np.sqrt(3.0) / 2.0], + ] + ) + return U @ vertices + + +def extract_summary(data: dict) -> dict: + if "summary" in data: + return data["summary"] + if "aggregated" in data: + return data["aggregated"] + raise KeyError("Result file must contain 'summary' or 'aggregated'") + + +def metric_mean(summary: dict, method: str, metric: str) -> float: + return float(summary[method][metric]["mean"]) + + +def metric_std(summary: dict, method: str, metric: str) -> float: + return float(summary[method][metric]["std"]) + + +def available_methods(summary: dict) -> list[str]: + return [m for m in METHOD_ORDER if m in summary] + + +def highlight_best_cells(ax, matrix: np.ndarray, methods: list[str], exclude: set[str] | None = None): + exclude = exclude or set() + for col in range(matrix.shape[1]): + best_row = None + best_val = None + for row, method in enumerate(methods): + val = matrix[row, col] + if method in exclude or np.isnan(val): + continue + if best_val is None or val < best_val: + best_val = val + best_row = row + if best_row is None: + continue + ax.add_patch( + Rectangle( + (col - 0.5, best_row - 0.5), + 1.0, + 1.0, + fill=False, + edgecolor="black", + linewidth=1.5, + ) + ) + + +def load_suite(results_dir: Path) -> dict[str, dict]: + suite = {} + for stem, _ in DGP_SPECS: + path = results_dir / f"{stem}.json" + if path.exists(): + data = load_json(path) + summary = extract_summary(data) + merged = {"summary": summary, "raw_data": data} + + if stem in SYNTH_EXTRA_FILES: + for extra_name in SYNTH_EXTRA_FILES[stem]: + extra_path = results_dir / extra_name + if not extra_path.exists(): + continue + extra_data = load_json(extra_path) + extra_summary = extract_summary(extra_data) + merged["summary"] = {**merged["summary"], **extra_summary} + merged.setdefault("extra_raw_data", {})[extra_name] = extra_data + + suite[stem] = merged + return suite + + +def load_real_suite(results_dir: Path) -> dict[str, dict]: + suite = {} + for filename, task in REAL_SPECS: + path = results_dir / filename + if not path.exists(): + continue + data = load_json(path) + summary = extract_summary(data) + merged = {"summary": summary, "raw_data": data} + + if task in REAL_EXTRA_FILES: + for extra_name in REAL_EXTRA_FILES[task]: + extra_path = results_dir / extra_name + if not extra_path.exists(): + continue + extra_data = load_json(extra_path) + extra_summary = extract_summary(extra_data) + merged["summary"] = {**merged["summary"], **extra_summary} + merged.setdefault("extra_raw_data", {})[extra_name] = extra_data + + suite[task] = merged + return suite + + +def fig1_allocation_geometry(suite: dict[str, dict], output_dir: Path): + """Illustrate simplex allocation failure on the smooth-scale synthetic regime.""" + stem = "d2_pure_scale" + task_path = REPO_ROOT / "release" / "simplextasks-12" / "synthetic" / stem / "task.npz" + if stem not in suite or not task_path.exists(): + print("Skipping Fig 1 allocation geometry: D2 task or summary missing") + return + + task = np.load(task_path) + U = task["U"] + sigma_true = task["sigma_true"] + xy = simplex_to_xy(U) + summary = suite[stem]["summary"] + strata_keys = sorted(summary["global"]["stratified_coverage"].keys(), key=int) + x = np.arange(len(strata_keys)) + + rng = np.random.default_rng(2026) + sample_idx = rng.choice(len(U), size=min(2500, len(U)), replace=False) + xy_sample = xy[sample_idx] + sigma_sample = sigma_true[sample_idx] + + fig = plt.figure(figsize=(8.0, 2.95), constrained_layout=True) + gs = fig.add_gridspec(1, 3, width_ratios=[1.12, 0.92, 1.18]) + ax0 = fig.add_subplot(gs[0, 0]) + ax1 = fig.add_subplot(gs[0, 1]) + ax2 = fig.add_subplot(gs[0, 2]) + + sc = ax0.scatter( + xy_sample[:, 0], + xy_sample[:, 1], + c=sigma_sample, + cmap="viridis", + s=8, + alpha=0.8, + linewidths=0.0, + ) + triangle = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [0.5, np.sqrt(3.0) / 2.0], + [0.0, 0.0], + ] + ) + ax0.plot(triangle[:, 0], triangle[:, 1], color="black", linewidth=1.0) + ax0.text(-0.03, -0.03, r"$u_1$", ha="right", va="top") + ax0.text(1.03, -0.03, r"$u_2$", ha="left", va="top") + ax0.text(0.5, np.sqrt(3.0) / 2.0 + 0.04, r"$u_3$", ha="center", va="bottom") + ax0.set_title("D2 local scale on the simplex") + ax0.set_aspect("equal") + ax0.set_xlim(-0.08, 1.08) + ax0.set_ylim(-0.08, np.sqrt(3.0) / 2.0 + 0.1) + ax0.axis("off") + cbar = fig.colorbar(sc, ax=ax0, fraction=0.046, pad=0.02) + cbar.set_label(r"True local scale $\sigma(u)$") + + target = 0.9 + global_cov = [summary["global"]["stratified_coverage"][k]["mean"] for k in strata_keys] + ax1.bar(x, global_cov, color=METHOD_COLORS["global"], alpha=0.88, width=0.72) + ax1.axhline(target, color="black", linestyle="--", linewidth=1) + ax1.set_title("Global CP allocates poorly") + ax1.set_xticks(x) + ax1.set_xticklabels([f"S{k}" for k in strata_keys]) + ax1.set_xlabel(r"Boundary strata ($S0 \rightarrow S4$)") + ax1.set_ylabel("Coverage by stratum") + ax1.set_ylim(0.0, 1.02) + ax1.grid(axis="y", color="#d9d9d9", linewidth=0.7, alpha=0.7) + + comparison_methods = ["global", "partition", "twostage"] + for method in comparison_methods: + vals = [summary[method]["stratified_coverage"][k]["mean"] for k in strata_keys] + ax2.plot( + x, + vals, + color=METHOD_COLORS[method], + linewidth=1.8, + marker=PROFILE_MARKERS.get(method, "o"), + markersize=4.5, + label=METHOD_LABELS[method], + ) + ax2.axhline(target, color="black", linestyle="--", linewidth=1) + ax2.set_title("Repair depends on the regime") + ax2.set_xticks(x) + ax2.set_xticklabels([f"S{k}" for k in strata_keys]) + ax2.set_xlabel(r"Boundary strata ($S0 \rightarrow S4$)") + ax2.set_ylim(0.0, 1.02) + ax2.grid(axis="y", color="#d9d9d9", linewidth=0.7, alpha=0.7) + ax2.legend(loc="lower right", frameon=False) + + for ax, label in zip([ax0, ax1, ax2], ["A", "B", "C"]): + ax.text(-0.12, 1.03, label, transform=ax.transAxes, fontsize=11, fontweight="bold") + + save_figure(fig, output_dir, "fig1_allocation_geometry.pdf") + plt.close(fig) + + +def fig1_disparity_heatmap(suite: dict[str, dict], output_dir: Path): + """Heatmap of max disparity across regimes and methods.""" + methods = METHOD_ORDER + matrix = np.full((len(methods), len(DGP_SPECS)), np.nan) + + for j, (stem, _) in enumerate(DGP_SPECS): + if stem not in suite: + continue + summary = extract_summary(suite[stem]) + for i, method in enumerate(methods): + if method in summary: + matrix[i, j] = metric_mean(summary, method, "max_disparity") + + fig, ax = plt.subplots(figsize=(7.0, 3.6), constrained_layout=True) + cmap = plt.cm.RdYlBu_r.copy() + cmap.set_bad("#efefef") + im = ax.imshow(np.ma.masked_invalid(matrix), aspect="auto", cmap=cmap, vmin=0.0, vmax=0.85) + + ax.set_xticks(range(len(DGP_SPECS))) + ax.set_xticklabels([label for _, label in DGP_SPECS]) + ax.set_yticks(range(len(methods))) + ax.set_yticklabels([METHOD_LABELS[m] for m in methods]) + ax.set_xticks(np.arange(-0.5, len(DGP_SPECS), 1), minor=True) + ax.set_yticks(np.arange(-0.5, len(methods), 1), minor=True) + ax.grid(which="minor", color="white", linewidth=1.0) + ax.tick_params(which="minor", bottom=False, left=False) + + for i in range(len(methods)): + for j in range(len(DGP_SPECS)): + val = matrix[i, j] + if np.isnan(val): + continue + txt_color = "white" if (val < 0.16 or val > 0.58) else "black" + stroke_color = "black" if txt_color == "white" else "white" + ax.text( + j, + i, + f"{val:.02f}", + ha="center", + va="center", + color=txt_color, + fontsize=7.5, + fontweight="bold", + path_effects=[pe.withStroke(linewidth=1.1, foreground=stroke_color)], + ) + + highlight_best_cells(ax, matrix, methods, exclude={"oracle", "weighted"}) + + cbar = fig.colorbar(im, ax=ax, shrink=0.95, pad=0.02) + cbar.set_label("Max disparity") + + save_figure(fig, output_dir, "fig1_synthetic_disparity_heatmap.pdf") + plt.close(fig) + + +def _plot_strata_panel(ax, data: dict, methods: list[str], title: str): + raw = data.get("raw_data", data) + config = raw.get("config", {}) + evaluation = config.get("evaluation", {}) + alpha = evaluation.get("alpha", config.get("alpha", 0.1)) + target = 1.0 - float(alpha) + summary = extract_summary(raw) + + reference_method = next(iter(summary)) + strata_keys = sorted(summary[reference_method]["stratified_coverage"].keys(), key=int) + x = np.arange(len(strata_keys)) + for method in methods: + if method not in summary: + continue + vals = [summary[method]["stratified_coverage"][k]["mean"] for k in strata_keys] + ax.plot( + x, + vals, + color=METHOD_COLORS[method], + linewidth=1.6, + marker=PROFILE_MARKERS.get(method, "o"), + markersize=4.2, + label=METHOD_LABELS[method], + ) + + ax.axhline(target, color="black", linestyle="--", linewidth=1) + ax.set_xticks(x) + ax.set_xticklabels([f"S{k}" for k in strata_keys]) + ax.set_ylim(0.0, 1.02) + ax.set_title(title) + ax.set_ylabel("Coverage by stratum") + ax.grid(axis="y", color="#d9d9d9", linewidth=0.7, alpha=0.7) + + +def fig2_stratified_profiles(suite: dict[str, dict], output_dir: Path): + """Representative stratified coverage plots for key regimes.""" + panels = [ + ("d2_pure_scale", ["global", "twostage", "oracle"], "D2: Smooth Scale"), + ("d3_discrete_groups_aligned", ["global", "partition", "oracle"], "D3: Aligned Discrete"), + ("d6_high_k", ["global", "partition", "twostage", "oracle"], "D6: High-K"), + ] + available_panels = [(stem, methods, title) for stem, methods, title in panels if stem in suite] + if not available_panels: + print("Skipping Fig 2: no matching synthetic results found") + return + + fig, axes = plt.subplots(1, len(available_panels), figsize=(3.4 * len(available_panels), 2.9), sharey=True, constrained_layout=True) + if len(available_panels) == 1: + axes = [axes] + + for ax, (stem, methods, title) in zip(axes, available_panels): + _plot_strata_panel(ax, suite[stem], methods, title) + + handles, labels = axes[0].get_legend_handles_labels() + fig.legend(handles, labels, ncol=min(4, len(labels)), loc="upper center", bbox_to_anchor=(0.5, 1.12), frameon=False) + + save_figure(fig, output_dir, "fig2_stratified_profiles.pdf") + plt.close(fig) + + +def fig3_regime_scatter(suite: dict[str, dict], output_dir: Path): + """Scatter of worst-stratum coverage vs disparity for selected methods.""" + selected_methods = ["global", "partition", "twostage", "fullcp", "jackknife_plus", "oracle"] + fig, ax = plt.subplots(figsize=(7.2, 4.4)) + + for stem, label in DGP_SPECS: + if stem not in suite: + continue + summary = extract_summary(suite[stem]) + for method in selected_methods: + if method not in summary: + continue + x = metric_mean(summary, method, "max_disparity") + y = metric_mean(summary, method, "worst_stratum_coverage") + ax.scatter( + x, + y, + s=65, + color=METHOD_COLORS[method], + alpha=0.9, + edgecolor="white", + linewidth=0.8, + ) + ax.text(x + 0.01, y, label.replace("\n", " "), fontsize=7, alpha=0.9) + + ax.axhline(0.9, color="black", linestyle="--", linewidth=1) + ax.set_xlabel("Max disparity") + ax.set_ylabel("Worst-stratum coverage") + ax.set_title("Synthetic Regimes: Fairness-Safety Tradeoff") + + legend_handles = [ + plt.Line2D([0], [0], marker="o", color="w", label=METHOD_LABELS[m], + markerfacecolor=METHOD_COLORS[m], markeredgecolor="white", markersize=8) + for m in selected_methods + if any(stem in suite and m in suite[stem]["summary"] for stem, _ in DGP_SPECS) + ] + ax.legend(handles=legend_handles, ncol=3, loc="lower left") + + save_figure(fig, output_dir, "fig3_regime_tradeoff.pdf") + plt.close(fig) + + +def fig4_runtime_tradeoff(suite: dict[str, dict], output_dir: Path): + """Runtime versus disparity on the smooth-scale benchmark.""" + stem = "d2_pure_scale" + if stem not in suite: + print("Skipping Fig 4: d2_pure_scale.json not found") + return + + summary = extract_summary(suite[stem]) + methods = available_methods(summary) + + fig, ax = plt.subplots(figsize=(6.0, 3.6), constrained_layout=True) + for method in methods: + x = metric_mean(summary, method, "runtime_sec") + y = metric_mean(summary, method, "max_disparity") + ax.scatter( + x, + y, + s=55, + color=METHOD_COLORS[method], + edgecolor="white", + linewidth=0.8, + marker=REAL_MARKERS.get(method, "o"), + ) + ax.text(x * 1.05 if x > 0 else x + 0.02, y, METHOD_LABELS[method], fontsize=7.3, va="center") + + ax.set_xlabel("Mean runtime per repetition (sec)") + ax.set_ylabel("Max disparity") + ax.set_xscale("symlog", linthresh=0.01) + ax.grid(color="#d9d9d9", linewidth=0.7, alpha=0.7) + + save_figure(fig, output_dir, "fig4_runtime_tradeoff.pdf") + plt.close(fig) + + +def fig5_real_disparity_heatmap(real_suite: dict[str, dict], output_dir: Path): + methods = ["global", "partition", "twostage", "fullcp", "jackknife_plus", "oneshot", "trainres", "weighted"] + tasks = [task for _, task in REAL_SPECS if task in real_suite] + if not tasks: + print("Skipping Fig 5: no real-data results found") + return + + matrix = np.full((len(methods), len(tasks)), np.nan) + for j, task in enumerate(tasks): + summary = real_suite[task]["summary"] + for i, method in enumerate(methods): + if method in summary and "max_disparity" in summary[method]: + matrix[i, j] = metric_mean(summary, method, "max_disparity") + + task_labels = { + "CIFAR-10": "CIFAR-10", + "Samson": "Samson", + "Topics": "Topics", + "AffectiveText": "Affective\nText", + "UTKFace": "UTKFace", + "PBMC": "PBMC", + } + + fig, ax = plt.subplots(figsize=(7.0, 4.0), constrained_layout=True) + cmap = plt.cm.RdYlBu_r.copy() + cmap.set_bad("#efefef") + im = ax.imshow(np.ma.masked_invalid(matrix), aspect="auto", cmap=cmap, vmin=0.0, vmax=0.9) + + ax.set_xticks(range(len(tasks))) + ax.set_xticklabels([task_labels.get(task, task) for task in tasks]) + ax.set_yticks(range(len(methods))) + ax.set_yticklabels([METHOD_LABELS[m] for m in methods]) + ax.set_xticks(np.arange(-0.5, len(tasks), 1), minor=True) + ax.set_yticks(np.arange(-0.5, len(methods), 1), minor=True) + ax.grid(which="minor", color="white", linewidth=1.0) + ax.tick_params(which="minor", bottom=False, left=False) + + for i in range(len(methods)): + for j in range(len(tasks)): + val = matrix[i, j] + if np.isnan(val): + continue + txt_color = "white" if (val < 0.16 or val > 0.58) else "black" + stroke_color = "black" if txt_color == "white" else "white" + ax.text( + j, + i, + f"{val:.02f}", + ha="center", + va="center", + color=txt_color, + fontsize=7.5, + fontweight="bold", + path_effects=[pe.withStroke(linewidth=1.1, foreground=stroke_color)], + ) + + highlight_best_cells(ax, matrix, methods, exclude={"weighted"}) + + cbar = fig.colorbar(im, ax=ax, shrink=0.95, pad=0.02) + cbar.set_label("Max disparity") + + save_figure(fig, output_dir, "fig5_real_disparity_heatmap.pdf") + plt.close(fig) + + +def fig6_real_tradeoff(real_suite: dict[str, dict], output_dir: Path): + selected_methods = ["global", "partition", "twostage", "jackknife_plus", "fullcp", "trainres"] + tasks = [task for _, task in REAL_SPECS if task in real_suite] + if not tasks: + print("Skipping Fig 6: no real-data results found") + return + + fig, axes = plt.subplots(2, 3, figsize=(7.1, 4.8), sharey=True) + axes = axes.flatten() + used_methods = set() + + for idx, ax in enumerate(axes): + if idx >= len(tasks): + ax.axis("off") + continue + task = tasks[idx] + summary = real_suite[task]["summary"] + xs = [] + for method in selected_methods: + if method not in summary or "mean_radius" not in summary[method]: + continue + x = metric_mean(summary, method, "mean_radius") + y = metric_mean(summary, method, "max_disparity") + xerr = metric_std(summary, method, "mean_radius") + yerr = metric_std(summary, method, "max_disparity") + xs.append(x) + ax.errorbar( + x, + y, + xerr=xerr, + yerr=yerr, + fmt="none", + ecolor=METHOD_COLORS[method], + elinewidth=0.9, + capsize=2.0, + alpha=0.28, + zorder=1, + ) + ax.scatter( + x, + y, + s=42, + marker=REAL_MARKERS.get(method, "o"), + color=METHOD_COLORS[method], + edgecolor="white", + linewidth=0.7, + alpha=0.96, + zorder=2, + ) + used_methods.add(method) + + if xs: + xmin = min(xs) + xmax = max(xs) + span = xmax - xmin + pad = 0.08 * span if span > 0 else max(0.05, 0.15 * xmax) + ax.set_xlim(max(0.0, xmin - pad), xmax + pad) + ax.set_ylim(0.0, 0.95) + ax.set_title(task) + ax.grid(color="#d9d9d9", linewidth=0.7, alpha=0.7) + if idx % 3 == 0: + ax.set_ylabel("Max disparity") + if idx >= 3: + ax.set_xlabel("Mean radius") + + handles = [ + plt.Line2D( + [0], [0], + marker=REAL_MARKERS.get(method, "o"), + color=METHOD_COLORS[method], + linestyle="None", + label=METHOD_LABELS[method], + markerfacecolor=METHOD_COLORS[method], + markeredgecolor="white", + markersize=6.5, + ) + for method in selected_methods + if method in used_methods + ] + fig.subplots_adjust(top=0.84, bottom=0.10, hspace=0.28, wspace=0.16) + fig.legend(handles=handles, ncol=3, loc="upper center", bbox_to_anchor=(0.5, 0.99), frameon=False) + + save_figure(fig, output_dir, "fig6_real_tradeoff.pdf") + plt.close(fig) + + +def fig7_real_profiles(real_suite: dict[str, dict], output_dir: Path): + panels = [ + ("CIFAR-10", ["global", "partition", "jackknife_plus"], "CIFAR-10"), + ("Topics", ["global", "twostage", "jackknife_plus"], "Topics"), + ("AffectiveText", ["global", "partition", "fullcp"], "AffectiveText"), + ("UTKFace", ["global", "partition", "jackknife_plus"], "UTKFace"), + ("PBMC", ["global", "partition", "twostage"], "PBMC"), + ] + available = [(task, methods, title) for task, methods, title in panels if task in real_suite] + if not available: + print("Skipping Fig 7: no matching real-data results found") + return + + fig, axes = plt.subplots(2, 3, figsize=(7.2, 4.8), sharey=True) + axes = axes.flatten() + + for idx, (task, methods, title) in enumerate(available): + ax = axes[idx] + summary = real_suite[task]["summary"] + alpha = 0.1 + target = 1.0 - alpha + reference_method = next(iter(summary)) + strata_keys = sorted(summary[reference_method]["stratified_coverage"].keys(), key=int) + x = np.arange(len(strata_keys)) + + for method in methods: + if method not in summary: + continue + vals = [summary[method]["stratified_coverage"][k]["mean"] for k in strata_keys] + ax.plot( + x, + vals, + color=METHOD_COLORS[method], + linewidth=1.5, + marker=PROFILE_MARKERS.get(method, "o"), + markersize=4.0, + label=METHOD_LABELS[method], + ) + + ax.axhline(target, color="black", linestyle="--", linewidth=1) + ax.set_xticks(x) + ax.set_xticklabels([f"S{k}" for k in strata_keys]) + ax.set_ylim(0.0, 1.02) + ax.set_title(title) + ax.grid(axis="y", color="#d9d9d9", linewidth=0.7, alpha=0.7) + if idx % 3 == 0: + ax.set_ylabel("Coverage by stratum") + + for idx in range(len(available), len(axes)): + axes[idx].axis("off") + + handles, labels = axes[0].get_legend_handles_labels() + fig.subplots_adjust(top=0.84, bottom=0.10, hspace=0.25, wspace=0.15) + fig.legend(handles, labels, ncol=min(4, len(labels)), loc="upper center", bbox_to_anchor=(0.5, 0.99), frameon=False) + + save_figure(fig, output_dir, "fig7_real_profiles.pdf") + plt.close(fig) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--results-dir", default="results/tables") + parser.add_argument("--output-dir", default="results/figures") + parser.add_argument("--fig", default="all", help="Which figure: lead,1,2,3,4,5,6,7,synthetic,real,all") + args = parser.parse_args() + + results_dir = Path(args.results_dir) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + suite = load_suite(results_dir) + real_suite = load_real_suite(results_dir) + + if args.fig in ("lead", "all", "synthetic"): + fig1_allocation_geometry(suite, output_dir) + if args.fig in ("1", "all", "synthetic"): + fig1_disparity_heatmap(suite, output_dir) + if args.fig in ("2", "all", "synthetic"): + fig2_stratified_profiles(suite, output_dir) + if args.fig in ("3", "all", "synthetic"): + fig3_regime_scatter(suite, output_dir) + if args.fig in ("4", "all", "synthetic"): + fig4_runtime_tradeoff(suite, output_dir) + if args.fig in ("5", "all", "real"): + fig5_real_disparity_heatmap(real_suite, output_dir) + if args.fig in ("6", "all", "real"): + fig6_real_tradeoff(real_suite, output_dir) + if args.fig in ("7", "all", "real"): + fig7_real_profiles(real_suite, output_dir) + + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/scripts/make_tables.py b/scripts/make_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..619d7512708c0aedfa5bf80d757410203f8adb9a --- /dev/null +++ b/scripts/make_tables.py @@ -0,0 +1,139 @@ +"""Generate paper-style summary tables from saved benchmark results.""" +import argparse +import json +from pathlib import Path + +METHOD_ORDER = [ + "global", + "partition", + "twostage", + "fullcp", + "jackknife_plus", + "oneshot", + "trainres", + "weighted", + "oracle", +] + +METHOD_LABELS = { + "global": "Global", + "partition": "Mondrian", + "twostage": "TwoStage", + "fullcp": "FullCP", + "jackknife_plus": "Jackknife+", + "oneshot": "OneShot", + "trainres": "TrainRes", + "weighted": "Weighted", + "oracle": "Oracle", +} + +SYNTHETIC_SPECS = [ + ("d1_homogeneous.json", "D1 Homogeneous"), + ("d2_pure_scale.json", "D2 Pure scale"), + ("d3_discrete_groups_aligned.json", "D3 Discrete aligned"), + ("d4_model_bias.json", "D4 Bias"), + ("d5_heavy_tail.json", "D5 Heavy tail"), + ("d6_high_k.json", "D6† High-K"), +] + +REAL_SPECS = [ + ("exp2_2_softmax_cifar10_strata_entropy_fixed.json", "CIFAR-10"), + ("exp2_3_hyperspectral_samson_nmf_all_methods.json", "Samson"), + ("exp2_5_topics_K10_all_methods.json", "Topics"), + ("exp2_6_affective_text.json", "AffectiveText"), + ("exp2_4_age_ldl_K10_image_knn_main.json", "UTKFace"), + ("real_bulk_deconv.json", "PBMC"), +] + +EXTRA_FILES = { + "D1 Homogeneous": ["d1_homogeneous_exact.json"], + "D3 Discrete aligned": ["d3_discrete_groups_aux.json"], + "D5 Heavy tail": ["d5_heavy_tail_aux.json"], + "D6† High-K": ["d6_high_k_aux.json", "d6_high_k_exact_appendix.json"], + "PBMC": ["real_bulk_deconv_fullcp.json", "real_bulk_deconv_aux.json", "real_bulk_deconv_trainres.json"], + "UTKFace": ["exp2_4_age_ldl_K10_image_knn_fullcp_2k.json"], +} + + +def load_json(path: Path) -> dict: + with open(path) as f: + return json.load(f) + + +def extract_summary(data: dict) -> dict: + if "summary" in data: + return data["summary"] + if "aggregated" in data: + return data["aggregated"] + raise KeyError("Missing summary/aggregated block") + + +def metric_cell(summary: dict, method: str) -> str: + if method not in summary: + return "--" + cov = summary[method]["marginal_coverage"]["mean"] + disp = summary[method]["max_disparity"]["mean"] + worst = summary[method]["worst_stratum_coverage"]["mean"] if "worst_stratum_coverage" in summary[method] else None + if worst is None and "stratified_coverage" in summary[method]: + worst = min(v["mean"] for v in summary[method]["stratified_coverage"].values()) + radius = summary[method]["mean_radius"]["mean"] if "mean_radius" in summary[method] else None + if radius is None: + return f"{cov:.3f} / {disp:.3f} / {worst:.3f}" + return f"{cov:.3f} / {disp:.3f} / {worst:.3f} / {radius:.3f}" + + +def write_markdown_table(out_path: Path, title: str, specs: list[tuple[str, str]], results_dir: Path, extras: dict[str, list[str]] | None = None): + rows = [] + for filename, label in specs: + path = results_dir / filename + if not path.exists(): + continue + summary = extract_summary(load_json(path)) + if extras and label in extras: + for extra_name in extras[label]: + extra_path = results_dir / extra_name + if extra_path.exists(): + summary = {**summary, **extract_summary(load_json(extra_path))} + row = [label] + for method in METHOD_ORDER: + row.append(metric_cell(summary, method)) + rows.append(row) + + headers = ["Task"] + [METHOD_LABELS[m] for m in METHOD_ORDER] + lines = [f"# {title}", "", "Cells report `coverage / disparity / worst-stratum / radius`.", ""] + lines.append("| " + " | ".join(headers) + " |") + lines.append("|" + "|".join(["---"] * len(headers)) + "|") + for row in rows: + lines.append("| " + " | ".join(row) + " |") + out_path.write_text("\n".join(lines) + "\n") + print(f"Saved {out_path}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--results-dir", default="results/tables") + parser.add_argument("--output-dir", default="results/tables") + args = parser.parse_args() + + results_dir = Path(args.results_dir) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + write_markdown_table( + output_dir / "paper_table_synthetic_summary.md", + "Synthetic Summary", + SYNTHETIC_SPECS, + results_dir, + extras=EXTRA_FILES, + ) + write_markdown_table( + output_dir / "paper_table_real_summary.md", + "Real-Data Summary", + REAL_SPECS, + results_dir, + extras=EXTRA_FILES, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/package_simplextasks_12.py b/scripts/package_simplextasks_12.py new file mode 100644 index 0000000000000000000000000000000000000000..9100d88a17bd03a6148a0c91a162a85fb21d301f --- /dev/null +++ b/scripts/package_simplextasks_12.py @@ -0,0 +1,438 @@ +"""Package benchmark tasks as the SimplexTasks-12 release artifact.""" +from __future__ import annotations + +import argparse +import json +import shutil +from pathlib import Path + +import numpy as np +import yaml + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from scripts.run_age_ldl import extract_image_features, get_age_predictions, load_utkface +from scripts.run_hyperspectral import load_hyperspectral, unmix_nmf +from scripts.run_topics import prepare_topic_data +from src.data import build_prediction_matrix, load_affective_text +from src.dgp.deconv import nnls_deconv +from src.dgp.discrete_groups import DiscreteGroupsDGP +from src.dgp.heavy_tail import HeavyTailDGP +from src.dgp.high_k import HighKDGP +from src.dgp.model_bias import ModelBiasDGP +from src.dgp.pseudobulk import generate_pseudobulk +from src.dgp.pure_scale import PureScaleDGP + +PACKAGE_NAME = "SimplexTasks-12" +PACKAGE_SLUG = "simplextasks-12" +VERSION = "0.1.0" +SEED = 2026 + +DGP_MAP = { + "pure_scale": PureScaleDGP, + "discrete_groups": DiscreteGroupsDGP, + "model_bias": ModelBiasDGP, + "heavy_tail": HeavyTailDGP, + "high_k": HighKDGP, +} + +SYNTHETIC_SPECS = [ + ("d1_homogeneous", "configs/synthetic/d1_homogeneous.yaml", "Homogeneous negative control"), + ("d2_pure_scale", "configs/synthetic/d2_pure_scale.yaml", "Smooth scale heterogeneity"), + ("d3_discrete_groups_aligned", "configs/synthetic/d3_discrete_groups_aligned.yaml", "Aligned discrete groups"), + ("d4_model_bias", "configs/synthetic/d4_model_bias.yaml", "Bias-type heterogeneity"), + ("d5_heavy_tail", "configs/synthetic/d5_heavy_tail.yaml", "Heavy-tail robustness"), + ("d6_high_k", "configs/synthetic/d6_high_k.yaml", "High-dimensional simplex"), +] + + +def _float32(x: np.ndarray) -> np.ndarray: + return x.astype(np.float32) if isinstance(x, np.ndarray) and x.dtype.kind == "f" else x + + +def save_npz(path: Path, arrays: dict[str, np.ndarray]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + np.savez_compressed(path, **{k: _float32(v) for k, v in arrays.items()}) + + +def write_json(path: Path, payload: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + +def write_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text.strip() + "\n", encoding="utf-8") + + +def build_cifar(root: Path) -> tuple[dict[str, np.ndarray], dict]: + cache_path = root / "data/processed/cifar10_resnet18_softmax.npz" + data = np.load(cache_path) + arrays = { + "Y": data["Y"], + "U": data["U"], + "class_names": data["class_names"], + "label_index": np.argmax(data["Y"], axis=1).astype(np.int16), + } + meta = { + "task_id": "cifar10_softmax", + "task_name": "CIFAR-10 softmax", + "subset": "real", + "n_samples": int(arrays["Y"].shape[0]), + "simplex_dim": int(arrays["Y"].shape[1]), + "source_asset": "CIFAR-10", + "predictor": "Frozen ResNet-18 softmax cache", + "redistribution": "derived-only", + "notes": "No raw images are redistributed in this package.", + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta + + +def build_topics() -> tuple[dict[str, np.ndarray], dict]: + Y, U = prepare_topic_data(K=10, seed=SEED) + arrays = { + "Y": Y, + "U": U, + "doc_index": np.arange(len(Y), dtype=np.int32), + } + meta = { + "task_id": "topics_20ng", + "task_name": "20 Newsgroups topics", + "subset": "real", + "n_samples": int(Y.shape[0]), + "simplex_dim": int(Y.shape[1]), + "source_asset": "20 Newsgroups", + "predictor": "TF-IDF to topic-mixture kNN regressor", + "redistribution": "derived-only", + "notes": "This release exposes only derived simplex arrays, not the raw text corpus.", + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta + + +def build_samson(root: Path) -> tuple[dict[str, np.ndarray], dict]: + data = load_hyperspectral(str(root / "data/raw/hyperspectral"), "samson") + U = unmix_nmf(data["pixels"], data["K"], seed=SEED) + arrays = { + "Y": data["abundances"], + "U": U, + "pixels": data["pixels"], + "endmembers": data["endmembers"], + "endmember_names": np.asarray(data["names"]), + "image_shape": np.asarray(data["shape"], dtype=np.int32), + } + meta = { + "task_id": "samson_unmixing", + "task_name": "Samson hyperspectral unmixing", + "subset": "real", + "n_samples": int(arrays["Y"].shape[0]), + "simplex_dim": int(arrays["Y"].shape[1]), + "source_asset": "Samson ROI hyperspectral benchmark", + "predictor": "NMF abundance estimator", + "redistribution": "source-cited", + "notes": "The public bundle did not ship an explicit license file; keep attribution with any downstream reuse.", + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta + + +def build_pbmc(root: Path) -> tuple[dict[str, np.ndarray], dict]: + import scanpy as sc + from sklearn.cluster import KMeans + from sklearn.decomposition import PCA + + h5ad_path = root / "data/pbmc3k_raw.h5ad" + adata = sc.read_h5ad(h5ad_path) + expr = adata.X + if hasattr(expr, "toarray"): + expr = expr.toarray() + expr = np.asarray(expr, dtype=np.float64) + + celltype_key = "cell_type" + if celltype_key in adata.obs.columns: + labels = adata.obs[celltype_key].values + else: + pca = PCA(n_components=30, random_state=42) + X_pca = pca.fit_transform(expr) + kmeans = KMeans(n_clusters=8, random_state=42, n_init=10) + labels = np.asarray([f"ct_{v}" for v in kmeans.fit_predict(X_pca)]) + cell_type_names = sorted(np.unique(labels).tolist()) + gene_names = adata.var_names.to_numpy() + + pb = generate_pseudobulk( + expr=expr, + labels=labels, + cell_type_names=cell_type_names, + gene_names=gene_names.tolist(), + n_samples=5000, + cells_per_sample=200, + concentration=1.0, + noise_sd=0.1, + seed=SEED, + ) + U = nnls_deconv(pb.bulk, pb.signature) + arrays = { + "Y": pb.proportions, + "U": U, + "bulk_expression": pb.bulk, + "signature": pb.signature, + "cell_type_names": np.asarray(pb.cell_type_names), + "gene_names": np.asarray(pb.gene_names), + } + meta = { + "task_id": "pbmc3k_pseudobulk", + "task_name": "PBMC3K pseudobulk deconvolution", + "subset": "real", + "n_samples": int(arrays["Y"].shape[0]), + "simplex_dim": int(arrays["Y"].shape[1]), + "source_asset": "PBMC3K single-cell reference", + "predictor": "NNLS deconvolution from pseudobulk mixtures", + "redistribution": "derived-only", + "notes": "This package exports pseudobulk-derived arrays and signatures rather than the raw single-cell matrix.", + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta + + +def build_utkface(root: Path) -> tuple[dict[str, np.ndarray], dict]: + data_dir = root / "data/raw/UTKFace" + ages, Y, image_paths = load_utkface(str(data_dir), K=10, sigma=2.0) + U = get_age_predictions(ages, Y, image_paths, K=10, method="image_knn", seed=SEED) + X_feat = extract_image_features(image_paths, image_size=16, cache_name=f"utkface_imgfeat_{len(image_paths)}_s16.npz") + arrays = { + "Y": Y, + "U": U, + "age": ages.astype(np.int16), + "image_features": X_feat, + } + meta = { + "task_id": "utkface_age_ldl", + "task_name": "UTKFace age label distributions", + "subset": "real", + "n_samples": int(arrays["Y"].shape[0]), + "simplex_dim": int(arrays["Y"].shape[1]), + "source_asset": "UTKFace aligned-and-cropped images", + "predictor": "Thumbnail feature PCA+kNN age-distribution regressor", + "redistribution": "derived-only", + "notes": "The package omits raw face images and keeps only derived features and simplex arrays.", + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta + + +def build_affective(root: Path) -> tuple[dict[str, np.ndarray], dict]: + data_dir = root / "data/raw/AffectiveText.Semeval.2007" + cache_path = root / "data/processed/affective_text_predictions.jsonl" + data = load_affective_text(data_dir) + pred_raw, U = build_prediction_matrix(data["ids"], cache_path) + arrays = { + "Y": data["Y"], + "U": U, + "gold_raw_scores": data["raw_scores"], + "pred_raw_scores": pred_raw, + "instance_id": np.asarray(data["ids"]), + "emotion_names": np.asarray(data["emotions"]), + } + meta = { + "task_id": "affectivetext_emotions", + "task_name": "SemEval AffectiveText emotions", + "subset": "real", + "n_samples": int(arrays["Y"].shape[0]), + "simplex_dim": int(arrays["Y"].shape[1]), + "source_asset": "SemEval-2007 Task 14 AffectiveText", + "predictor": "Frozen zero-shot emotion scorer; open TF-IDF+SVD+kNN fallback script available", + "redistribution": "derived-only", + "notes": "The package omits raw headlines and raw API responses, keeps only derived score arrays, and provides an open fallback cache builder in scripts/cache_affective_text_open_predictions.py.", + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta + + +def build_synthetic_task(root: Path, task_id: str, config_rel: str, regime_label: str) -> tuple[dict[str, np.ndarray], dict, Path]: + cfg_path = root / config_rel + cfg = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) + dgp_cfg = dict(cfg["dgp"]) + dgp_name = dgp_cfg.pop("name") + dgp = DGP_MAP[dgp_name](**dgp_cfg) + + data_cfg = cfg["data"] + n_train = int(data_cfg.get("n_train", 0)) + n_scale = int(data_cfg.get("n_scale_est", 0)) + n_cal = int(data_cfg.get("n_cal", 0)) + n_test = int(data_cfg.get("n_test", 0)) + n_total = n_train + n_scale + n_cal + n_test + + rng = np.random.default_rng(SEED) + sample = dgp.sample(n_total, rng) + split = np.concatenate( + [ + np.repeat("train", n_train), + np.repeat("scale", n_scale), + np.repeat("cal", n_cal), + np.repeat("test", n_test), + ] + ) + arrays = { + "X": sample.X, + "Y": sample.Y, + "U": sample.U, + "R": sample.R, + "sigma_true": sample.sigma_true if sample.sigma_true is not None else np.full(n_total, np.nan), + "split": split.astype("U8"), + } + meta = { + "task_id": task_id, + "task_name": task_id.replace("_", " "), + "subset": "synthetic", + "n_samples": int(sample.Y.shape[0]), + "simplex_dim": int(sample.Y.shape[1]), + "source_asset": "Synthetic DGP", + "predictor": "Oracle mean predictor from the configured DGP", + "redistribution": "open", + "regime_label": regime_label, + "config_file": config_rel, + "seed": SEED, + "available_arrays": sorted(arrays.keys()), + } + return arrays, meta, cfg_path + + +def write_task(task_dir: Path, arrays: dict[str, np.ndarray], metadata: dict) -> None: + save_npz(task_dir / "task.npz", arrays) + write_json(task_dir / "metadata.json", metadata) + + +def build_package_readme(manifest: dict) -> str: + real_lines = [] + for task in manifest["real_tasks"]: + real_lines.append(f"| `{task['task_id']}` | {task['task_name']} | {task['n_samples']} | {task['simplex_dim']} | {task['predictor']} |") + synthetic_lines = [] + for task in manifest["synthetic_tasks"]: + synthetic_lines.append(f"| `{task['task_id']}` | {task['regime_label']} | {task['n_samples']} | {task['simplex_dim']} |") + return f""" +--- +pretty_name: {PACKAGE_NAME} +license: other +task_categories: +- other +tags: +- conformal-prediction +- uncertainty-estimation +- simplex +- benchmark +- task-collection +--- + +# {PACKAGE_NAME} + +{PACKAGE_NAME} is the processed task collection behind the SimplexUQ benchmark. It packages 12 simplex-valued prediction tasks: 6 real tasks with fixed predictors and 6 synthetic regimes with canonical reference draws. + +## What is inside + +- Standardized `task.npz` files with at least `Y` and `U` for every task. +- Per-task `metadata.json` files with provenance, redistribution notes, and task-specific semantics. +- Canonical synthetic configs copied alongside the synthetic tasks. +- A `manifest.json` file that summarizes the full release. + +## Real Tasks + +| Task ID | Task | Samples | K | Predictor | +| --- | --- | ---: | ---: | --- | +{chr(10).join(real_lines)} + +## Synthetic Tasks + +| Task ID | Regime | Samples | K | +| --- | --- | ---: | ---: | +{chr(10).join(synthetic_lines)} + +## Redistribution Notes + +This package is release-oriented rather than raw-data-complete. Some tasks include only derived simplex arrays or derived features because the underlying source assets carry their own terms of use. In particular, raw UTKFace images, raw AffectiveText headlines, and the original CIFAR-10 image archive are not redistributed here. + +## Loading Example + +```python +from pathlib import Path +import numpy as np + +root = Path("{PACKAGE_SLUG}") +task = np.load(root / "real/cifar10_softmax/task.npz") +Y = task["Y"] +U = task["U"] +``` +""" + + +def build_license_notes() -> str: + return """ +# License and Usage Notes + +SimplexTasks-12 is a processed task collection. It should not be interpreted as a license override for the underlying source assets. + +- CIFAR-10: this release exposes derived simplex arrays only. +- 20 Newsgroups: this release exposes derived topic-mixture arrays only. +- AffectiveText: this release omits raw headlines and raw API responses. +- Samson: keep attribution with the source benchmark bundle. +- PBMC3K: this release exports derived pseudobulk and deconvolution artifacts. +- UTKFace: this release omits raw face images and keeps only derived features and simplex arrays. +""" + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output-dir", default=f"release/{PACKAGE_SLUG}") + parser.add_argument("--force", action="store_true") + args = parser.parse_args() + + root = Path(__file__).resolve().parent.parent + out_dir = root / args.output_dir + if out_dir.exists(): + if not args.force: + raise FileExistsError(f"{out_dir} already exists. Use --force to overwrite.") + shutil.rmtree(out_dir) + (out_dir / "real").mkdir(parents=True, exist_ok=True) + (out_dir / "synthetic").mkdir(parents=True, exist_ok=True) + + real_builders = [ + ("cifar10_softmax", lambda: build_cifar(root)), + ("topics_20ng", build_topics), + ("samson_unmixing", lambda: build_samson(root)), + ("pbmc3k_pseudobulk", lambda: build_pbmc(root)), + ("utkface_age_ldl", lambda: build_utkface(root)), + ("affectivetext_emotions", lambda: build_affective(root)), + ] + + real_manifest = [] + for task_id, builder in real_builders: + arrays, metadata = builder() + task_dir = out_dir / "real" / task_id + write_task(task_dir, arrays, metadata) + real_manifest.append(metadata) + + synthetic_manifest = [] + for task_id, cfg_rel, regime_label in SYNTHETIC_SPECS: + arrays, metadata, cfg_path = build_synthetic_task(root, task_id, cfg_rel, regime_label) + task_dir = out_dir / "synthetic" / task_id + write_task(task_dir, arrays, metadata) + shutil.copy2(cfg_path, task_dir / "config.yaml") + synthetic_manifest.append(metadata) + + manifest = { + "name": PACKAGE_NAME, + "slug": PACKAGE_SLUG, + "version": VERSION, + "seed": SEED, + "task_count": len(real_manifest) + len(synthetic_manifest), + "real_tasks": real_manifest, + "synthetic_tasks": synthetic_manifest, + } + write_json(out_dir / "manifest.json", manifest) + write_text(out_dir / "README.md", build_package_readme(manifest)) + write_text(out_dir / "LICENSE_NOTES.md", build_license_notes()) + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_pbmc_sensitivity.py b/scripts/plot_pbmc_sensitivity.py new file mode 100644 index 0000000000000000000000000000000000000000..3602116763aa5ed76ae297553fc23ffd67e0dc4c --- /dev/null +++ b/scripts/plot_pbmc_sensitivity.py @@ -0,0 +1,155 @@ +"""Plot PBMC sensitivity analyses for appendix use.""" +import argparse +import json +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + + +plt.rcParams.update({ + "font.size": 9, + "font.family": "sans-serif", + "font.sans-serif": ["DejaVu Sans", "Arial"], + "axes.labelsize": 10, + "axes.titlesize": 10, + "legend.fontsize": 8, + "xtick.labelsize": 8, + "ytick.labelsize": 8, + "figure.dpi": 150, + "savefig.dpi": 300, + "savefig.bbox": "tight", + "axes.spines.top": False, + "axes.spines.right": False, +}) + +METHODS = ["global", "partition", "twostage", "fullcp"] +METHOD_LABELS = { + "global": "Global", + "partition": "Mondrian", + "twostage": "TwoStage", + "fullcp": "FullCP", +} + +SETTING_GROUPS = { + "Stratification": [ + ("Boundary", "results/tables/pbmc_sensitivity_exp2_1_bulk_deconv_boundary_fixed.json"), + ("Entropy", "results/tables/pbmc_sensitivity_exp2_1_bulk_deconv_entropy_fixed.json"), + ("KMeans", "results/tables/pbmc_sensitivity_exp2_1_bulk_deconv_kmeans_fixed.json"), + ], + "Mixture concentration": [ + ("0.5", "results/tables/pbmc_concentration_sparse.json"), + ("1.0", ["results/tables/real_bulk_deconv.json", "results/tables/real_bulk_deconv_fullcp.json"]), + ("2.0", "results/tables/pbmc_concentration_balanced.json"), + ], +} + +LINE_COLORS = { + "Boundary": "#0072B2", + "Entropy": "#D55E00", + "KMeans": "#009E73", + "0.5": "#CC79A7", + "1.0": "#0072B2", + "2.0": "#E69F00", +} + +LINE_MARKERS = { + "Boundary": "o", + "Entropy": "s", + "KMeans": "D", + "0.5": "o", + "1.0": "s", + "2.0": "D", +} + +REPO_ROOT = Path(__file__).resolve().parents[1] +PAPER_FIG_DIR = REPO_ROOT / "paper" / "rewrite_2026" / "latex" / "figures" + + +def load_summary(path_or_paths: str | list[str]) -> dict: + if isinstance(path_or_paths, str): + path_or_paths = [path_or_paths] + + merged = {} + for path in path_or_paths: + with open(path) as f: + data = json.load(f) + summary = data["aggregated"] if "aggregated" in data else data["summary"] + merged.update(summary) + return merged + + +def extract_metric(summary: dict, method: str, metric: str) -> tuple[float, float]: + entry = summary[method][metric] + return float(entry["mean"]), float(entry["std"]) + + +def plot_panel(ax, title: str, settings: list[tuple[str, str]]): + x = np.arange(len(METHODS)) + offsets = np.linspace(-0.22, 0.22, num=len(settings)) + + for offset, (label, path) in zip(offsets, settings): + summary = load_summary(path) + y = [] + yerr = [] + for method in METHODS: + mean, std = extract_metric(summary, method, "max_disparity") + y.append(mean) + yerr.append(std) + ax.errorbar( + x + offset, + y, + yerr=yerr, + color=LINE_COLORS[label], + marker=LINE_MARKERS[label], + markersize=5.5, + linewidth=1.6, + elinewidth=1.0, + capsize=2.5, + label=label, + ) + + ax.set_title(title) + ax.set_xticks(x) + ax.set_xticklabels([METHOD_LABELS[m] for m in METHODS]) + ax.set_ylabel("Max disparity") + ax.set_ylim(0.0, 0.55) + ax.grid(axis="y", color="#d9d9d9", linewidth=0.8) + + +def save_figure(fig: plt.Figure, output: Path) -> None: + """Save the appendix figure to both the results tree and the paper tree.""" + output.parent.mkdir(parents=True, exist_ok=True) + PAPER_FIG_DIR.mkdir(parents=True, exist_ok=True) + fig.savefig(output) + mirror = PAPER_FIG_DIR / output.name + fig.savefig(mirror) + print(f"Saved {output}") + print(f"Mirrored {mirror}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--output", + default="results/figures/fig8_pbmc_sensitivity.pdf", + help="Output figure path", + ) + args = parser.parse_args() + + fig, axes = plt.subplots(1, 2, figsize=(7.1, 2.9), constrained_layout=True) + for ax, (title, settings) in zip(axes, SETTING_GROUPS.items()): + plot_panel(ax, title, settings) + + handles, labels = axes[1].get_legend_handles_labels() + fig.legend(handles, labels, loc="upper center", ncol=3, frameon=False, bbox_to_anchor=(0.5, 1.05)) + + out = Path(args.output) + save_figure(fig, out) + plt.close(fig) + + +if __name__ == "__main__": + main() diff --git a/scripts/reproduce_figures.py b/scripts/reproduce_figures.py new file mode 100644 index 0000000000000000000000000000000000000000..d9ee6f609af9b77edda3a50721b6786da79cc806 --- /dev/null +++ b/scripts/reproduce_figures.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT.parent / 'SimplexTasks-12-data' +RESULTS = ROOT / 'results' / 'tables' +OUTPUTS = ROOT / 'outputs' / 'figures' + + +def stage_inputs() -> None: + if not DATA.exists(): + raise SystemExit('SimplexTasks-12-data bundle not found next to the code bundle') + RESULTS.mkdir(parents=True, exist_ok=True) + OUTPUTS.mkdir(parents=True, exist_ok=True) + for subdir in ['figure_inputs', 'table_inputs']: + src_dir = DATA / 'data' / 'summaries' / subdir + if not src_dir.exists(): + continue + for src in src_dir.iterdir(): + if src.is_file(): + shutil.copy2(src, RESULTS / src.name) + + d2_dir = DATA / 'data' / 'synthetic' / 'D2' + if d2_dir.exists(): + release_dir = ROOT / 'release' / 'simplextasks-12' / 'synthetic' / 'd2_pure_scale' + release_dir.mkdir(parents=True, exist_ok=True) + np.savez( + release_dir / 'task.npz', + U=np.load(d2_dir / 'U.npy'), + sigma_true=np.load(d2_dir / 'sigma_true.npy'), + ) + + +def main() -> None: + stage_inputs() + commands = [ + [sys.executable, str(ROOT / 'scripts' / 'make_figures.py'), '--results-dir', str(RESULTS), '--output-dir', str(OUTPUTS)], + [sys.executable, str(ROOT / 'scripts' / 'plot_pbmc_sensitivity.py'), '--output', str(OUTPUTS / 'fig8_pbmc_sensitivity.pdf')], + ] + for cmd in commands: + subprocess.check_call(cmd, cwd=ROOT) + + +if __name__ == '__main__': + main() diff --git a/scripts/reproduce_tables.py b/scripts/reproduce_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..6d69fc51232c70108ad838617d47aecc3d867655 --- /dev/null +++ b/scripts/reproduce_tables.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT.parent / 'SimplexTasks-12-data' +RESULTS = ROOT / 'results' / 'tables' +OUTPUTS = ROOT / 'outputs' / 'tables' +PAPER = ROOT / 'paper' / 'rewrite_2026' / 'latex' + + +def stage_inputs() -> None: + if not DATA.exists(): + raise SystemExit('SimplexTasks-12-data bundle not found next to the code bundle') + RESULTS.mkdir(parents=True, exist_ok=True) + OUTPUTS.mkdir(parents=True, exist_ok=True) + PAPER.mkdir(parents=True, exist_ok=True) + for subdir in ['figure_inputs', 'table_inputs']: + src_dir = DATA / 'data' / 'summaries' / subdir + if not src_dir.exists(): + continue + for src in src_dir.iterdir(): + if src.is_file(): + shutil.copy2(src, RESULTS / src.name) + + +def main() -> None: + stage_inputs() + commands = [ + [sys.executable, str(ROOT / 'scripts' / 'make_tables.py'), '--results-dir', str(RESULTS), '--output-dir', str(OUTPUTS)], + [ + sys.executable, + str(ROOT / 'scripts' / 'summarize_real_strata_sensitivity.py'), + '--input-dir', str(RESULTS), + '--detail-csv', str(OUTPUTS / 'real_strata_sensitivity_detail.csv'), + '--winner-json', str(OUTPUTS / 'real_strata_sensitivity_winners.json'), + '--winner-md', str(OUTPUTS / 'real_strata_sensitivity_summary.md'), + '--winner-tex', str(OUTPUTS / 'generated_real_strata_sensitivity.tex'), + ], + [sys.executable, str(ROOT / 'scripts' / 'make_appendix_tables.py')], + ] + for cmd in commands: + subprocess.check_call(cmd, cwd=ROOT) + appendix_tex = PAPER / 'generated_appendix_tables.tex' + if appendix_tex.exists(): + shutil.copy2(appendix_tex, OUTPUTS / appendix_tex.name) + + +if __name__ == '__main__': + main() diff --git a/scripts/run_affective_text.py b/scripts/run_affective_text.py new file mode 100644 index 0000000000000000000000000000000000000000..fb8b472bfd291c62945971ebaa7b060a7a8a528b --- /dev/null +++ b/scripts/run_affective_text.py @@ -0,0 +1,330 @@ +"""Run conformal benchmark on SemEval-2007 Affective Text emotion compositions.""" +from __future__ import annotations + +import argparse +import json +import logging +import time +from pathlib import Path + +import numpy as np + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.data import build_prediction_matrix, load_affective_text +from src.methods import ( + full_conformal, + global_split_conformal, + jackknife_plus_conformal, + oneshot_conformal, + partition_conformal, + trainres_conformal, + twostage_conformal, + weighted_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from src.metrics.coverage import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from src.metrics.setsize import mean_radius, mean_volume_ratio, volume_ratio_by_strata +from src.metrics.sscv import size_stratified_coverage_violation +from src.utils.seed import get_rng +from src.utils.simplex import aitchison_dist +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_boundary, + stratify_by_entropy, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +DEFAULT_METHODS = [ + "global", + "partition", + "twostage", + "jackknife_plus", + "weighted", + "oneshot", + "trainres", + "fullcp", +] + + +def compute_weight_vectors(R_cal, U_cal, U_test, k=20): + sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal, k=k) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=k) + weights_cal = 1.0 / np.maximum(sigma_cal, 1e-8) + weights_test = 1.0 / np.maximum(sigma_test, 1e-8) + weights_cal /= np.mean(weights_cal) + weights_test /= np.mean(weights_test) + return weights_cal, weights_test + + +def macro_pearson(a: np.ndarray, b: np.ndarray) -> float: + vals = [] + for j in range(a.shape[1]): + aj = a[:, j] + bj = b[:, j] + if np.std(aj) <= 1e-12 or np.std(bj) <= 1e-12: + continue + vals.append(float(np.corrcoef(aj, bj)[0, 1])) + return float(np.mean(vals)) if vals else float("nan") + + +def run_experiment( + Y, + U, + alpha, + n_rep, + cal_frac, + n_strata, + rng, + methods, + strata_mode, + compute_volume=False, + volume_score="aitchison", + volume_n_mc=50000, + volume_max_points=None, + fixed_strata=True, + strata_seed=2026, +): + R = aitchison_dist(Y, U) + n = len(R) + n_cal = int(n * cal_frac) + all_results = {m: [] for m in methods} + fixed_labels = None + if fixed_strata: + fixed_labels = precompute_fixed_strata(U, strata_mode, n_strata, seed=strata_seed) + elif strata_mode not in {"boundary", "entropy"}: + raise ValueError("Non-fixed AffectiveText strata must be 'boundary' or 'entropy'.") + else: + strata_fn = stratify_by_boundary if strata_mode == "boundary" else stratify_by_entropy + + for rep in range(n_rep): + perm = rng.permutation(n) + idx_cal, idx_test = perm[:n_cal], perm[n_cal:] + + R_cal, R_test = R[idx_cal], R[idx_test] + U_cal, U_test = U[idx_cal], U[idx_test] + + if fixed_labels is not None: + strata_cal = fixed_labels[idx_cal] + strata_test = fixed_labels[idx_test] + else: + strata_cal = strata_fn(U_cal, n_strata) + strata_test = strata_fn(U_test, n_strata) + weights_cal, weights_test = compute_weight_vectors(R_cal, U_cal, U_test) + + for m in methods: + start = time.perf_counter() + if m == "global": + res = global_split_conformal(R_cal, R_test, alpha) + elif m == "partition": + res = partition_conformal(R_cal, R_test, alpha, strata_cal, strata_test) + elif m == "twostage": + res = twostage_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "jackknife_plus": + res = jackknife_plus_conformal(R_cal, R_test, alpha, U_cal=U_cal, U_test=U_test) + elif m == "weighted": + res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + elif m == "oneshot": + res = oneshot_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "trainres": + train_perm = rng.permutation(n) + idx_train = train_perm[:n_cal] + res = trainres_conformal(R_cal, R_test, alpha, U_cal, U_test, R[idx_train], U[idx_train]) + elif m == "fullcp": + res = full_conformal(R_cal, R_test, alpha, U_cal, U_test) + else: + continue + runtime_sec = time.perf_counter() - start + all_results[m].append(dict( + marginal_coverage=float(marginal_coverage(res.covered)), + max_disparity=float(max_disparity(res.covered, strata_test, alpha)), + worst_stratum_coverage=float(worst_stratum_coverage(res.covered, strata_test)), + mean_radius=float(mean_radius(res.radius)), + sscv=float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), + coverage_variance=float(coverage_variance(res.covered, strata_test)), + runtime_sec=float(runtime_sec), + stratified_coverage={str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items()}, + )) + if compute_volume: + all_results[m][-1]["mean_volume_ratio"] = float( + mean_volume_ratio( + U_test, + res.radius, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ) + ) + all_results[m][-1]["volume_ratio_by_strata"] = { + str(k): float(v) + for k, v in volume_ratio_by_strata( + U_test, + res.radius, + strata_test, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ).items() + } + if (rep + 1) % 50 == 0: + log.info(f" Rep {rep + 1}/{n_rep}") + return all_results + + +def summarize_results(all_results: dict, methods: list[str]) -> dict: + summary = {} + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + "mean_volume_ratio", + ] + for m in methods: + reps = all_results.get(m, []) + if not reps: + continue + s = {} + for key in scalar_keys: + if key in reps[0]: + vals = [r[key] for r in reps] + s[key] = {"mean": float(np.mean(vals)), "std": float(np.std(vals))} + strata_keys = set() + for r in reps: + strata_keys.update(r["stratified_coverage"].keys()) + s["stratified_coverage"] = { + k: { + "mean": float(np.mean([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "std": float(np.std([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "n_reps": int(sum(k in r["stratified_coverage"] for r in reps)), + } + for k in sorted(strata_keys, key=int) + } + if "volume_ratio_by_strata" in reps[0]: + vol_keys = set() + for r in reps: + vol_keys.update(r["volume_ratio_by_strata"].keys()) + s["volume_ratio_by_strata"] = { + k: { + "mean": float(np.mean([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "std": float(np.std([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "n_reps": int(sum(k in r["volume_ratio_by_strata"] for r in reps)), + } + for k in sorted(vol_keys, key=int) + } + summary[m] = s + return summary + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--data-dir", default="data/raw/AffectiveText.Semeval.2007") + parser.add_argument("--prediction-cache", default="data/processed/affective_text_predictions.jsonl") + parser.add_argument("--alpha", type=float, default=0.1) + parser.add_argument("--n_rep", type=int, default=200) + parser.add_argument("--cal_frac", type=float, default=0.4) + parser.add_argument("--n_strata", type=int, default=5) + parser.add_argument( + "--strata", + choices=["boundary", "entropy", "dominant", "kmeans", "random"], + default="boundary", + ) + parser.add_argument("--fixed-strata", dest="fixed_strata", action="store_true") + parser.add_argument( + "--separate-strata", + dest="fixed_strata", + action="store_false", + help="Diagnostic only: fit calibration/test strata separately.", + ) + parser.set_defaults(fixed_strata=True) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--tag", default=None) + parser.add_argument("--output-dir", default="results") + parser.add_argument("--methods", nargs="+", default=DEFAULT_METHODS, choices=DEFAULT_METHODS) + parser.add_argument("--compute-volume", action="store_true") + parser.add_argument("--volume-score", choices=["aitchison", "tv"], default="aitchison") + parser.add_argument("--volume-n-mc", type=int, default=50000) + parser.add_argument("--volume-max-points", type=int, default=None) + args = parser.parse_args() + + data = load_affective_text(args.data_dir) + pred_raw, U = build_prediction_matrix(data["ids"], args.prediction_cache) + Y = data["Y"] + gold_raw = data["raw_scores"] + rng = get_rng(args.seed) + + R = aitchison_dist(Y, U) + macro_r = macro_pearson(gold_raw, pred_raw) + flat_r = float(np.corrcoef(gold_raw.reshape(-1), pred_raw.reshape(-1))[0, 1]) + log.info(f"Loaded {len(Y)} headlines with cached predictions") + log.info(f"Predictor quality: macro Pearson={macro_r:.3f}, flattened Pearson={flat_r:.3f}") + log.info(f"Residuals: mean={R.mean():.4f}, std={R.std():.4f}") + + all_results = run_experiment( + Y, + U, + args.alpha, + args.n_rep, + args.cal_frac, + args.n_strata, + rng, + args.methods, + args.strata, + fixed_strata=args.fixed_strata, + compute_volume=args.compute_volume, + volume_score=args.volume_score, + volume_n_mc=args.volume_n_mc, + volume_max_points=args.volume_max_points, + strata_seed=args.seed, + ) + summary = summarize_results(all_results, args.methods) + + log.info("\n" + "=" * 60) + log.info("RESULTS — SemEval-2007 Affective Text") + log.info("=" * 60) + for m in args.methods: + if m not in summary: + continue + s = summary[m] + log.info( + f" {m:12s} cov={s['marginal_coverage']['mean']:.3f}±{s['marginal_coverage']['std']:.3f} " + f"disp={s['max_disparity']['mean']:.3f}±{s['max_disparity']['std']:.3f}" + ) + + out_dir = Path(args.output_dir) / "tables" + out_dir.mkdir(parents=True, exist_ok=True) + suffix = f"_{args.tag}" if args.tag else "" + out_file = out_dir / f"exp2_6_affective_text{suffix}.json" + with open(out_file, "w", encoding="utf-8") as f: + json.dump( + dict( + summary=summary, + n=len(Y), + K=Y.shape[1], + emotions=data["emotions"], + predictor_quality=dict(macro_pearson=macro_r, flattened_pearson=flat_r), + config=vars(args), + raw=all_results, + ), + f, + indent=2, + ) + log.info(f"Saved to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_age_ldl.py b/scripts/run_age_ldl.py new file mode 100644 index 0000000000000000000000000000000000000000..dae5834d975021e8b24ebff93eed37172ac32ec4 --- /dev/null +++ b/scripts/run_age_ldl.py @@ -0,0 +1,527 @@ +"""Exp 2.4 — Label distribution learning: facial age estimation. + +Predict age distribution (soft label over age bins) from facial images. +Ground truth is a Gaussian-smoothed label over K age bins. + +Uses UTKFace dataset (free, no registration needed). + +Usage: + python scripts/run_age_ldl.py --data-dir data/raw/UTKFace + python scripts/run_age_ldl.py --data-dir data/raw/UTKFace --K 10 +""" +import argparse +import json +import logging +import numpy as np +from pathlib import Path +import re +import time + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.utils.simplex import aitchison_dist +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_boundary, + stratify_by_entropy, +) +from src.utils.seed import get_rng +from src.methods import ( + full_conformal, + global_split_conformal, + jackknife_plus_conformal, + oneshot_conformal, + partition_conformal, + trainres_conformal, + twostage_conformal, + weighted_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from src.metrics.coverage import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from src.metrics.sscv import size_stratified_coverage_violation +from src.metrics.setsize import mean_radius, mean_volume_ratio, volume_ratio_by_strata + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +DEFAULT_METHODS = [ + "global", + "partition", + "twostage", + "jackknife_plus", + "weighted", + "oneshot", + "trainres", +] + + +def age_to_soft_label(age: int, K: int = 10, age_range: tuple = (0, 100), + sigma: float = 2.0) -> np.ndarray: + """Convert integer age to Gaussian-smoothed distribution over K bins. + + Args: + age: integer age + K: number of bins + age_range: (min_age, max_age) + sigma: smoothing in bin units + + Returns: + distribution over K bins, sums to 1 + """ + bin_edges = np.linspace(age_range[0], age_range[1], K + 1) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + bin_width = bin_edges[1] - bin_edges[0] + + # Gaussian kernel centered at true age + probs = np.exp(-0.5 * ((bin_centers - age) / (sigma * bin_width)) ** 2) + probs = probs / probs.sum() + # Floor for numerical safety + probs = np.maximum(probs, 1e-8) + probs = probs / probs.sum() + return probs + + +def load_utkface(data_dir: str, K: int = 10, sigma: float = 2.0): + """Load UTKFace dataset and create soft labels. + + UTKFace filename format: [age]_[gender]_[race]_[date&time].jpg + + Download from: https://susanqq.github.io/UTKFace/ + Or: kaggle datasets download jangedoo/utkface-new + + Returns: + ages: integer ages (n,) + Y: soft labels (n, K) + image_paths: list of paths (for optional feature extraction) + """ + data_dir = Path(data_dir) + files = list(data_dir.glob("*.jpg")) + list(data_dir.glob("*.png")) + + if not files: + raise FileNotFoundError( + f"No images found in {data_dir}. " + "Download UTKFace from https://susanqq.github.io/UTKFace/" + ) + + ages = [] + valid_files = [] + for f in files: + # Parse age from filename + parts = f.stem.split("_") + try: + age = int(parts[0]) + if 0 <= age <= 100: + ages.append(age) + valid_files.append(f) + except (ValueError, IndexError): + continue + + ages = np.array(ages) + Y = np.array([age_to_soft_label(a, K=K, sigma=sigma) for a in ages]) + + log.info(f"Loaded {len(ages)} images, age range [{ages.min()}, {ages.max()}]") + log.info(f"Soft labels: K={K}, sigma={sigma}") + + return ages, Y, valid_files + + +def extract_image_features( + image_paths: list, + image_size: int = 16, + cache_name: str | None = None, +): + """Extract compact image features from UTKFace files. + + The representation is intentionally lightweight: RGB thumbnail pixels plus + a few global summary statistics. This keeps the benchmark CPU-friendly + while making the predictor depend on image content rather than the target + age metadata. + """ + from PIL import Image + + cache_path = None + if cache_name is not None: + cache_path = Path("data/processed") / cache_name + if cache_path.exists(): + log.info(f"Loading cached UTKFace image features from {cache_path}") + return np.load(cache_path)["X"] + + feats = [] + for path in image_paths: + with Image.open(path) as img: + img = img.convert("RGB").resize((image_size, image_size)) + arr = np.asarray(img, dtype=np.float32) / 255.0 + rgb_flat = arr.reshape(-1) + gray = arr.mean(axis=2) + stats = np.array([ + gray.mean(), + gray.std(), + arr[..., 0].mean(), + arr[..., 1].mean(), + arr[..., 2].mean(), + ], dtype=np.float32) + feats.append(np.concatenate([rgb_flat, stats])) + + X = np.asarray(feats, dtype=np.float32) + if cache_path is not None: + cache_path.parent.mkdir(parents=True, exist_ok=True) + np.savez_compressed(cache_path, X=X) + log.info(f"Cached UTKFace image features to {cache_path}") + return X + + +def get_age_predictions(ages: np.ndarray, Y: np.ndarray, image_paths: list, + K: int, method: str = "knn", seed: int = 2026): + """Get predicted age distributions. + + Methods: + - 'knn': use age as feature, kNN regression in label space (diagnostic baseline) + - 'image_knn': use thumbnail image features + PCA + kNN regression + - 'noisy': add noise to true labels (controlled experiment) + - 'cnn': train a CNN (requires GPU, optional) + + Returns: + U: predicted distributions (n, K) + """ + if method == "noisy": + # Add heteroscedastic noise: more noise for middle ages + rng = np.random.default_rng(seed) + noise_scale = 0.05 + 0.15 * np.abs(ages - 50) / 50 # more noise at extremes + noise = rng.normal(0, noise_scale[:, None], Y.shape) + U = Y + noise + U = np.maximum(U, 1e-8) + U = U / U.sum(axis=1, keepdims=True) + return U + + elif method == "image_knn": + from sklearn.decomposition import PCA + from sklearn.neighbors import KNeighborsRegressor + from sklearn.pipeline import make_pipeline + from sklearn.preprocessing import StandardScaler + + cache_name = f"utkface_imgfeat_{len(image_paths)}_s16.npz" + X = extract_image_features(image_paths, image_size=16, cache_name=cache_name) + + rng = np.random.default_rng(seed) + n = len(ages) + train_idx = rng.choice(n, size=int(0.8 * n), replace=False) + + pca_dim = min(64, X.shape[1], len(train_idx)) + model = make_pipeline( + StandardScaler(), + PCA(n_components=pca_dim, random_state=seed), + KNeighborsRegressor(n_neighbors=25, weights="distance"), + ) + model.fit(X[train_idx], Y[train_idx]) + U = model.predict(X) + U = np.maximum(U, 1e-8) + U = U / U.sum(axis=1, keepdims=True) + return U + + elif method == "knn": + from sklearn.neighbors import KNeighborsRegressor + # Use age as the sole feature, predict soft label + X = ages.reshape(-1, 1) + # Leave-one-out style: train on 80%, predict on all + rng = np.random.default_rng(seed) + n = len(ages) + train_idx = rng.choice(n, size=int(0.8 * n), replace=False) + model = KNeighborsRegressor(n_neighbors=20, weights="distance") + model.fit(X[train_idx], Y[train_idx]) + U = model.predict(X) + U = np.maximum(U, 1e-8) + U = U / U.sum(axis=1, keepdims=True) + return U + + elif method == "cnn": + raise ValueError( + "CNN predictor training is outside this fixed-predictor artifact. " + "Use 'image_knn', 'knn', or 'noisy'." + ) + + else: + raise ValueError(f"Unknown method: {method}") + + +def compute_weight_vectors(R_cal, U_cal, U_test, k=20): + sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal, k=k) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=k) + weights_cal = 1.0 / np.maximum(sigma_cal, 1e-8) + weights_test = 1.0 / np.maximum(sigma_test, 1e-8) + weights_cal /= np.mean(weights_cal) + weights_test /= np.mean(weights_test) + return weights_cal, weights_test + + +def run_experiment( + Y, + U, + alpha, + n_rep, + cal_frac, + n_strata, + rng, + methods, + compute_volume=False, + volume_score="aitchison", + volume_n_mc=50000, + volume_max_points=None, + strata_method="entropy", + fixed_strata=True, + strata_seed=2026, +): + """Standard conformal experiment.""" + R = aitchison_dist(Y, U) + n = len(R) + n_cal = int(n * cal_frac) + + all_results = {m: [] for m in methods} + fixed_labels = None + if fixed_strata: + fixed_labels = precompute_fixed_strata(U, strata_method, n_strata, seed=strata_seed) + elif strata_method not in {"boundary", "entropy"}: + raise ValueError("Non-fixed age strata must be 'boundary' or 'entropy'.") + + for rep in range(n_rep): + perm = rng.permutation(n) + idx_cal, idx_test = perm[:n_cal], perm[n_cal:] + + R_cal, R_test = R[idx_cal], R[idx_test] + U_cal, U_test = U[idx_cal], U[idx_test] + + if fixed_labels is not None: + strata_cal = fixed_labels[idx_cal] + strata_test = fixed_labels[idx_test] + else: + strata_fn = stratify_by_boundary if strata_method == "boundary" else stratify_by_entropy + strata_cal = strata_fn(U_cal, n_strata) + strata_test = strata_fn(U_test, n_strata) + weights_cal, weights_test = compute_weight_vectors(R_cal, U_cal, U_test) + + for m in methods: + start = time.perf_counter() + if m == "global": + res = global_split_conformal(R_cal, R_test, alpha) + elif m == "partition": + res = partition_conformal(R_cal, R_test, alpha, + strata_cal, strata_test) + elif m == "twostage": + res = twostage_conformal(R_cal, R_test, alpha, + U_cal, U_test) + elif m == "jackknife_plus": + res = jackknife_plus_conformal(R_cal, R_test, alpha, U_cal=U_cal, U_test=U_test) + elif m == "weighted": + res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + elif m == "oneshot": + res = oneshot_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "trainres": + train_perm = rng.permutation(n) + idx_train = train_perm[:n_cal] + res = trainres_conformal( + R_cal, R_test, alpha, U_cal, U_test, R[idx_train], U[idx_train] + ) + elif m == "fullcp": + res = full_conformal(R_cal, R_test, alpha, U_cal, U_test) + else: + continue + + runtime_sec = time.perf_counter() - start + all_results[m].append(dict( + marginal_coverage=float(marginal_coverage(res.covered)), + max_disparity=float(max_disparity(res.covered, strata_test, alpha)), + worst_stratum_coverage=float(worst_stratum_coverage(res.covered, strata_test)), + mean_radius=float(mean_radius(res.radius)), + sscv=float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), + coverage_variance=float(coverage_variance(res.covered, strata_test)), + runtime_sec=float(runtime_sec), + stratified_coverage={ + str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items() + }, + )) + if compute_volume: + all_results[m][-1]["mean_volume_ratio"] = float( + mean_volume_ratio( + U_test, + res.radius, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ) + ) + all_results[m][-1]["volume_ratio_by_strata"] = { + str(k): float(v) + for k, v in volume_ratio_by_strata( + U_test, + res.radius, + strata_test, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ).items() + } + + if (rep + 1) % 50 == 0: + log.info(f" Rep {rep + 1}/{n_rep}") + + return all_results + + +def maybe_subsample(ages, Y, image_paths, max_samples, rng): + if max_samples is None or max_samples >= len(Y): + return ages, Y, image_paths + idx = rng.choice(len(Y), size=max_samples, replace=False) + idx = np.sort(idx) + return ages[idx], Y[idx], [image_paths[i] for i in idx] + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--data-dir", default="data/raw/UTKFace") + parser.add_argument("--K", type=int, default=10, help="Number of age bins") + parser.add_argument("--sigma", type=float, default=2.0, help="Label smoothing width") + parser.add_argument( + "--pred-method", + default="image_knn", + choices=["image_knn", "knn", "noisy", "cnn"], + ) + parser.add_argument("--alpha", type=float, default=0.1) + parser.add_argument("--n_rep", type=int, default=200) + parser.add_argument("--cal_frac", type=float, default=0.4) + parser.add_argument("--n_strata", type=int, default=5) + parser.add_argument( + "--strata", + choices=["entropy", "boundary", "dominant", "kmeans", "random"], + default="entropy", + ) + parser.add_argument("--fixed-strata", dest="fixed_strata", action="store_true") + parser.add_argument( + "--separate-strata", + dest="fixed_strata", + action="store_false", + help="Diagnostic only: fit calibration/test strata separately.", + ) + parser.set_defaults(fixed_strata=True) + parser.add_argument("--max_samples", type=int, default=None) + parser.add_argument( + "--methods", + nargs="+", + default=DEFAULT_METHODS, + choices=DEFAULT_METHODS + ["fullcp"], + ) + parser.add_argument("--tag", default=None) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--output-dir", default="results") + parser.add_argument("--compute-volume", action="store_true") + parser.add_argument("--volume-score", choices=["aitchison", "tv"], default="aitchison") + parser.add_argument("--volume-n-mc", type=int, default=50000) + parser.add_argument("--volume-max-points", type=int, default=None) + args = parser.parse_args() + + rng = get_rng(args.seed) + + # Load data + ages, Y, image_paths = load_utkface(args.data_dir, K=args.K, sigma=args.sigma) + ages, Y, image_paths = maybe_subsample(ages, Y, image_paths, args.max_samples, rng) + + # Get predictions + log.info(f"Getting predictions (method={args.pred_method})...") + U = get_age_predictions(ages, Y, image_paths, K=args.K, + method=args.pred_method, seed=args.seed) + + R = aitchison_dist(Y, U) + log.info(f"Residuals: mean={R.mean():.4f}, std={R.std():.4f}") + + # Run + all_results = run_experiment( + Y, + U, + args.alpha, + args.n_rep, + args.cal_frac, + args.n_strata, + rng, + args.methods, + compute_volume=args.compute_volume, + volume_score=args.volume_score, + volume_n_mc=args.volume_n_mc, + volume_max_points=args.volume_max_points, + strata_method=args.strata, + fixed_strata=args.fixed_strata, + strata_seed=args.seed, + ) + + # Report + log.info("\n" + "=" * 60) + log.info(f"RESULTS — Age LDL (K={args.K}, method={args.pred_method})") + log.info("=" * 60) + + summary = {} + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + "mean_volume_ratio", + ] + for m in args.methods: + if not all_results[m]: + continue + reps = all_results[m] + s = {} + for key in scalar_keys: + if key in reps[0]: + vals = [r[key] for r in reps] + s[key] = {"mean": float(np.mean(vals)), "std": float(np.std(vals))} + strata_keys = set() + for r in reps: + strata_keys.update(r["stratified_coverage"].keys()) + s["stratified_coverage"] = { + k: { + "mean": float(np.mean([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "std": float(np.std([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "n_reps": int(sum(k in r["stratified_coverage"] for r in reps)), + } + for k in sorted(strata_keys, key=int) + } + if "volume_ratio_by_strata" in reps[0]: + vol_keys = set() + for r in reps: + vol_keys.update(r["volume_ratio_by_strata"].keys()) + s["volume_ratio_by_strata"] = { + k: { + "mean": float(np.mean([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "std": float(np.std([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "n_reps": int(sum(k in r["volume_ratio_by_strata"] for r in reps)), + } + for k in sorted(vol_keys, key=int) + } + summary[m] = s + log.info( + f" {m:12s} cov={s['marginal_coverage']['mean']:.3f}±{s['marginal_coverage']['std']:.3f} " + f"disp={s['max_disparity']['mean']:.3f}±{s['max_disparity']['std']:.3f}" + ) + + out_dir = Path(args.output_dir) / "tables" + out_dir.mkdir(parents=True, exist_ok=True) + suffix = f"_{args.tag}" if args.tag else "" + out_file = out_dir / f"exp2_4_age_ldl_K{args.K}{suffix}.json" + with open(out_file, "w") as f: + json.dump(dict(summary=summary, K=args.K, n=len(ages), + config=vars(args), raw=all_results), f, indent=2) + log.info(f"Saved to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_all_synthetic.py b/scripts/run_all_synthetic.py new file mode 100644 index 0000000000000000000000000000000000000000..33ef8effa8249144d52143d05b47742097f76def --- /dev/null +++ b/scripts/run_all_synthetic.py @@ -0,0 +1,32 @@ +"""Run the synthetic SimplexUQ benchmark suite.""" +import argparse +import subprocess +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--pattern", + default="d[1-6]_*.yaml", + help="Glob pattern under configs/synthetic/ for benchmark configs", + ) + parser.add_argument( + "--python", + default="python", + help="Python executable used to launch run_synthetic.py", + ) + args = parser.parse_args() + + config_dir = Path("configs/synthetic") + configs = sorted(config_dir.glob(args.pattern)) + if not configs: + raise SystemExit(f"No configs matched {config_dir / args.pattern}") + + for cfg in configs: + print(f"\n{'=' * 72}\nRunning {cfg}\n{'=' * 72}") + subprocess.run([args.python, "scripts/run_synthetic.py", "--config", str(cfg)], check=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_benchmark.py b/scripts/run_benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..41803ed89877ecd5853de0632a73fccc93088bd0 --- /dev/null +++ b/scripts/run_benchmark.py @@ -0,0 +1,10 @@ +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +COMMANDS = [ + [sys.executable, str(ROOT / 'scripts' / 'run_all_synthetic.py')], +] +for cmd in COMMANDS: + subprocess.check_call(cmd) diff --git a/scripts/run_bulk_deconv.py b/scripts/run_bulk_deconv.py new file mode 100644 index 0000000000000000000000000000000000000000..b0cb0c5fd9025af0e9fd354790e21d0e7daac64e --- /dev/null +++ b/scripts/run_bulk_deconv.py @@ -0,0 +1,307 @@ +"""Run bulk deconvolution conformal prediction experiments. + +Exp 2.1: Semi-synthetic bulk RNA-seq deconvolution. +- Load PBMC3K reference +- Generate pseudo-bulk samples with known cell type proportions (ONCE) +- NNLS deconvolution → residuals (ONCE) +- 200 reps = different random cal/test splits of the same residuals +""" +import argparse +import json +import logging +import time +from pathlib import Path +import sys + +import numpy as np +import scanpy as sc +import yaml + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.dgp.deconv import nnls_deconv +from src.dgp.pseudobulk import generate_pseudobulk +from src.methods import ( + jackknife_plus_conformal, + global_split_conformal, + oneshot_conformal, + partition_conformal, + trainres_conformal, + weighted_conformal, + twostage_conformal, + full_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat +from src.metrics import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from src.metrics import mean_radius, radius_by_strata +from src.metrics.sscv import size_stratified_coverage_violation +from src.utils.simplex import aitchison_dist +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_boundary, + stratify_by_entropy, + stratify_by_kmeans, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +STRATA_REGISTRY = { + "boundary": stratify_by_boundary, + "entropy": stratify_by_entropy, + "kmeans": stratify_by_kmeans, +} + + +def run_one_rep( + R: np.ndarray, + U: np.ndarray, + cfg: dict, + rep_idx: int, + base_seed: int, + fixed_labels: np.ndarray | None = None, +) -> dict: + """Run one repetition: random cal/test split + conformal methods.""" + alpha = cfg["conformal"]["alpha"] + n_strata = cfg["evaluation"]["n_strata"] + strata_method = cfg["evaluation"]["strata_method"] + cal_frac = cfg["conformal"]["cal_frac"] + + n_samples = len(R) + seed = base_seed + rep_idx + rng = np.random.default_rng(seed) + + # Random cal/test split + n_cal = int(n_samples * cal_frac) + idx = rng.permutation(n_samples) + idx_cal, idx_test = idx[:n_cal], idx[n_cal:] + + R_cal, R_test = R[idx_cal], R[idx_test] + U_cal, U_test = U[idx_cal], U[idx_test] + + # Stratification on test set + if fixed_labels is not None: + strata_test = fixed_labels[idx_test] + else: + strata_fn = STRATA_REGISTRY[strata_method] + strata_test = strata_fn(U_test, n_strata) + + rep_results = {} + for method_name in cfg["conformal"]["methods"]: + start = time.perf_counter() + if method_name == "global": + result = global_split_conformal(R_cal, R_test, alpha) + elif method_name == "partition": + if fixed_labels is not None: + strata_cal = fixed_labels[idx_cal] + else: + strata_cal = strata_fn(U_cal, n_strata) + result = partition_conformal(R_cal, R_test, alpha, strata_cal, strata_test) + elif method_name == "twostage": + n_scale_est = len(R_cal) // 2 + result = twostage_conformal(R_cal, R_test, alpha, U_cal, U_test, n_scale_est=n_scale_est) + elif method_name == "fullcp": + result = full_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif method_name == "jackknife_plus": + result = jackknife_plus_conformal(R_cal, R_test, alpha, U_cal=U_cal, U_test=U_test) + elif method_name == "oneshot": + result = oneshot_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif method_name == "weighted": + sigma_cal = knn_sigma_hat(U_cal, R_cal, U_cal) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test) + floor = float(np.mean(sigma_cal) * 0.1) + weights_cal = 1.0 / np.maximum(sigma_cal, floor) + weights_test = 1.0 / np.maximum(sigma_test, floor) + result = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + elif method_name == "trainres": + train_perm = rng.permutation(n_samples) + idx_train = train_perm[:n_cal] + result = trainres_conformal( + R_cal, R_test, alpha, U_cal, U_test, R[idx_train], U[idx_train] + ) + else: + continue + runtime_sec = time.perf_counter() - start + + rep_results[method_name] = { + "marginal_coverage": float(marginal_coverage(result.covered)), + "max_disparity": float(max_disparity(result.covered, strata_test, alpha)), + "worst_stratum_coverage": float(worst_stratum_coverage(result.covered, strata_test)), + "stratified_coverage": { + str(k): float(v) + for k, v in stratified_coverage(result.covered, strata_test).items() + }, + "mean_radius": float(mean_radius(result.radius)), + "sscv": float(size_stratified_coverage_violation(result.covered, result.radius, alpha)), + "coverage_variance": float(coverage_variance(result.covered, strata_test)), + "runtime_sec": float(runtime_sec), + "radius_by_strata": { + str(k): float(v) + for k, v in radius_by_strata(result.radius, strata_test).items() + }, + } + + return rep_results + + +def aggregate_reps(all_reps: list[dict]) -> dict: + """Aggregate metrics across repetitions.""" + methods = all_reps[0].keys() + agg = {} + for method in methods: + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + ] + agg[method] = {} + for key in scalar_keys: + values = [rep[method][key] for rep in all_reps] + agg[method][key] = {"mean": float(np.mean(values)), "std": float(np.std(values))} + + # Aggregate per-strata coverage (some reps may lack certain strata) + all_strata_keys: set[str] = set() + for rep in all_reps: + all_strata_keys.update(rep[method]["stratified_coverage"].keys()) + agg[method]["stratified_coverage"] = {} + for s in sorted(all_strata_keys): + vals = [ + rep[method]["stratified_coverage"][s] + for rep in all_reps + if s in rep[method]["stratified_coverage"] + ] + if vals: + agg[method]["stratified_coverage"][s] = { + "mean": float(np.mean(vals)), + "std": float(np.std(vals)), + "n_reps": len(vals), + } + + # Aggregate per-strata radius + all_radius_keys: set[str] = set() + for rep in all_reps: + all_radius_keys.update(rep[method]["radius_by_strata"].keys()) + agg[method]["radius_by_strata"] = {} + for s in sorted(all_radius_keys): + vals = [ + rep[method]["radius_by_strata"][s] + for rep in all_reps + if s in rep[method]["radius_by_strata"] + ] + if vals: + agg[method]["radius_by_strata"][s] = { + "mean": float(np.mean(vals)), + "std": float(np.std(vals)), + "n_reps": len(vals), + } + + return agg + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + args = parser.parse_args() + + with open(args.config) as f: + cfg = yaml.safe_load(f) + + exp_name = cfg["experiment"] + log.info(f"Running experiment: {exp_name}") + + # ── Step 1: Load PBMC3K reference ── + log.info("Loading PBMC3K reference data...") + adata = sc.datasets.pbmc3k() + log.info(f" Loaded {adata.n_obs} cells, {adata.n_vars} genes") + + celltype_key = cfg["data"]["celltype_key"] + expr = adata.X + if hasattr(expr, "toarray"): + expr = expr.toarray() + expr = np.asarray(expr, dtype=np.float64) + + if celltype_key not in adata.obs.columns: + log.info(f" '{celltype_key}' not found, adding via KMeans clustering...") + from sklearn.decomposition import PCA + from sklearn.cluster import KMeans + pca = PCA(n_components=30, random_state=42) + X_pca = pca.fit_transform(expr) + kmeans = KMeans(n_clusters=8, random_state=42, n_init=10) + adata.obs[celltype_key] = "ct_" + kmeans.fit_predict(X_pca).astype(str) + log.info(f" Created {adata.obs[celltype_key].nunique()} cell type clusters") + + cell_type_names = sorted(np.unique(adata.obs[celltype_key].values)) + gene_names = adata.var_names.tolist() + labels = adata.obs[celltype_key].values + + # ── Step 2: Generate pseudo-bulk + deconvolve (ONCE) ── + base_seed = cfg["seed"] + log.info("Generating pseudo-bulk dataset (once)...") + pb = generate_pseudobulk( + expr=expr, + labels=labels, + cell_type_names=cell_type_names, + gene_names=gene_names, + n_samples=cfg["data"]["n_samples"], + cells_per_sample=cfg["data"]["cells_per_sample"], + concentration=cfg["data"]["concentration"], + noise_sd=cfg["data"]["noise_sd"], + seed=base_seed, + ) + log.info(f" Generated {pb.bulk.shape[0]} samples, {len(cell_type_names)} types") + + log.info("Running NNLS deconvolution (once)...") + U = nnls_deconv(pb.bulk, pb.signature) + log.info(f" Deconvolved {U.shape[0]} samples") + + R = aitchison_dist(pb.proportions, U) + log.info(f" Computed residuals: mean={R.mean():.3f}, std={R.std():.3f}") + + # ── Step 3: 200 reps = different random cal/test splits ── + n_reps = cfg["conformal"]["n_reps"] + log.info(f"Running {n_reps} conformal reps (split-only)...") + fixed_labels = None + if cfg["evaluation"].get("fixed_strata", True): + fixed_labels = precompute_fixed_strata( + U, + cfg["evaluation"]["strata_method"], + cfg["evaluation"]["n_strata"], + seed=base_seed, + ) + + all_reps = [] + for i in range(n_reps): + rep_results = run_one_rep(R, U, cfg, i, base_seed, fixed_labels=fixed_labels) + all_reps.append(rep_results) + if (i + 1) % 50 == 0: + log.info(f" Completed {i + 1}/{n_reps} reps") + + agg = aggregate_reps(all_reps) + + # ── Save results ── + out_dir = Path("results/tables") + out_dir.mkdir(parents=True, exist_ok=True) + out_path = out_dir / f"{exp_name}.json" + with open(out_path, "w") as f: + json.dump({"config": cfg, "aggregated": agg, "raw": all_reps}, f, indent=2) + + log.info(f"Results saved to {out_path}") + for method, metrics in agg.items(): + cov = metrics["marginal_coverage"] + disp = metrics["max_disparity"] + log.info(f" {method}: coverage={cov['mean']:.3f}±{cov['std']:.3f}, " + f"disparity={disp['mean']:.3f}±{disp['std']:.3f}") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_hyperspectral.py b/scripts/run_hyperspectral.py new file mode 100644 index 0000000000000000000000000000000000000000..6a7a50a52ad3c4add84e7ff61690b8f540d5bd31 --- /dev/null +++ b/scripts/run_hyperspectral.py @@ -0,0 +1,491 @@ +"""Hyperspectral unmixing experiment for conformal prediction on the simplex. + +Benchmark datasets: Samson (K=3), Jasper Ridge (K=4), Urban (K=4-6) +Each pixel's abundance vector ∈ Δ^{K-1}, ground truth available. + +Usage: + python scripts/run_hyperspectral.py --dataset samson + python scripts/run_hyperspectral.py --dataset jasper +""" +import argparse +import json +import logging +import numpy as np +from pathlib import Path +import time +from scipy.io import loadmat +from scipy.optimize import nnls + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.utils.simplex import aitchison_dist +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_boundary, + stratify_by_entropy, +) +from src.utils.seed import get_rng +from src.methods import ( + full_conformal, + global_split_conformal, + jackknife_plus_conformal, + oneshot_conformal, + oracle_conformal, + partition_conformal, + trainres_conformal, + twostage_conformal, + weighted_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from src.metrics.coverage import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from src.metrics.sscv import size_stratified_coverage_violation +from src.metrics.setsize import mean_radius, mean_volume_ratio, volume_ratio_by_strata + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +DEFAULT_METHODS = [ + "global", + "partition", + "twostage", + "jackknife_plus", + "weighted", + "oneshot", + "trainres", + "oracle", +] + +# ===================================================================== +# Dataset configs +# ===================================================================== +DATASETS = { + "samson": dict( + data_file="samson/samson_1.mat", + endmember_file="samson/end3.mat", + abundance_file="samson/end3.mat", # A is in same file as M for wispcarey data + data_key="V", # (n_bands, n_pixels) = (156, 9025) + endmember_key="M", # (n_bands, K) = (156, 3) + abundance_key="A", # (K, n_pixels) = (3, 9025) + n_rows=95, n_cols=95, + K=3, + names=["Soil", "Tree", "Water"], + ), + "jasper": dict( + data_file="jasper/jasperRidge2_R198.mat", + endmember_file="jasper/end4.mat", + abundance_file="jasper/end4.mat", # A is in same file as M for wispcarey data + data_key="Y", # (198, 10000) uint16 + endmember_key="M", # (198, 4) + abundance_key="A", # (4, 10000) + n_rows=100, n_cols=100, + K=4, + names=["Tree", "Water", "Dirt", "Road"], + ), +} + + +def load_hyperspectral(data_dir: str, dataset: str) -> dict: + """Load hyperspectral image, endmembers, and ground truth abundances. + + Returns: + dict with: + pixels: (n_pixels, n_bands) - spectral data + endmembers: (n_bands, K) - endmember spectra + abundances: (n_pixels, K) - ground truth fractions + names: list of endmember names + shape: (n_rows, n_cols) + """ + cfg = DATASETS[dataset] + data_dir = Path(data_dir) + + # Load image data + img_mat = loadmat(str(data_dir / cfg["data_file"])) + pixels = img_mat[cfg["data_key"]].astype(np.float64) + # Ensure (n_pixels, n_bands) + if pixels.shape[0] < pixels.shape[1]: + pixels = pixels.T + + # Load endmembers + end_mat = loadmat(str(data_dir / cfg["endmember_file"])) + endmembers = end_mat[cfg["endmember_key"]].astype(np.float64) + # Ensure (n_bands, K) + if endmembers.shape[1] > endmembers.shape[0]: + endmembers = endmembers.T + + # Load abundances + abund_mat = loadmat(str(data_dir / cfg["abundance_file"])) + abundances = abund_mat[cfg["abundance_key"]].astype(np.float64) + # Ensure (n_pixels, K) + if abundances.shape[0] == cfg["K"]: + abundances = abundances.T + + n_pixels = cfg["n_rows"] * cfg["n_cols"] + + # Truncate/reshape if needed + pixels = pixels[:n_pixels] + abundances = abundances[:n_pixels] + + # Normalize abundances to sum to 1 (they should already, but ensure) + row_sums = abundances.sum(axis=1, keepdims=True) + abundances = abundances / np.maximum(row_sums, 1e-10) + + log.info(f"Dataset: {dataset}") + log.info(f" Pixels: {pixels.shape} ({cfg['n_rows']}x{cfg['n_cols']})") + log.info(f" Bands: {endmembers.shape[0]}") + log.info(f" Endmembers ({cfg['K']}): {cfg['names']}") + log.info(f" Abundance range: [{abundances.min():.4f}, {abundances.max():.4f}]") + + return dict( + pixels=pixels, + endmembers=endmembers, + abundances=abundances, + names=cfg["names"], + shape=(cfg["n_rows"], cfg["n_cols"]), + K=cfg["K"], + ) + + +def unmix_nnls(pixels: np.ndarray, endmembers: np.ndarray) -> np.ndarray: + """NNLS unmixing: for each pixel, solve min ||pixel - E @ a||^2, a >= 0. + + Args: + pixels: (n_pixels, n_bands) + endmembers: (n_bands, K) + + Returns: + abundances_hat: (n_pixels, K), normalized to simplex + """ + n = pixels.shape[0] + K = endmembers.shape[1] + props = np.zeros((n, K)) + + for i in range(n): + coef, _ = nnls(endmembers, pixels[i]) + total = coef.sum() + props[i] = coef / total if total > 0 else np.ones(K) / K + + return props + + +def unmix_nmf(pixels: np.ndarray, K: int, seed: int = 2026) -> np.ndarray: + """NMF-based unmixing: estimate endmembers AND abundances from data. + + Unlike NNLS with known endmembers, NMF introduces endmember estimation + error, producing heterogeneous residuals across the simplex. + + Args: + pixels: (n_pixels, n_bands) + K: number of endmembers + + Returns: + abundances_hat: (n_pixels, K), normalized to simplex + """ + from sklearn.decomposition import NMF + + log.info(f" Running NMF with K={K} components...") + nmf = NMF(n_components=K, init="nndsvda", max_iter=500, + random_state=seed, l1_ratio=0.5) + W = nmf.fit_transform(pixels) # (n_pixels, K) — abundance-like + # H = nmf.components_ # (K, n_bands) — endmember-like + + # Normalize rows to simplex + W = np.maximum(W, 1e-10) + U = W / W.sum(axis=1, keepdims=True) + + recon_err = nmf.reconstruction_err_ + log.info(f" NMF reconstruction error: {recon_err:.4f}") + + return U + + +def run_experiment( + Y: np.ndarray, # ground truth (n, K) + U: np.ndarray, # predictions (n, K) + alpha: float, + n_rep: int, + cal_frac: float, + n_strata: int, + rng, + methods, + compute_volume: bool = False, + volume_score: str = "aitchison", + volume_n_mc: int = 20000, + volume_max_points: int | None = None, + strata_method: str = "boundary", + fixed_strata: bool = True, + strata_seed: int = 2026, +): + """Run conformal experiment with repeated cal/test splits.""" + R = aitchison_dist(Y, U) + n = len(R) + n_cal = int(n * cal_frac) + + all_results = {m: [] for m in methods} + fixed_labels = None + if fixed_strata: + fixed_labels = precompute_fixed_strata(U, strata_method, n_strata, seed=strata_seed) + elif strata_method not in {"boundary", "entropy"}: + raise ValueError("Non-fixed hyperspectral strata must be 'boundary' or 'entropy'.") + + for rep in range(n_rep): + perm = rng.permutation(n) + idx_cal, idx_test = perm[:n_cal], perm[n_cal:] + + R_cal, R_test = R[idx_cal], R[idx_test] + U_cal, U_test = U[idx_cal], U[idx_test] + + if fixed_labels is not None: + strata_cal = fixed_labels[idx_cal] + strata_test = fixed_labels[idx_test] + else: + strata_fn = stratify_by_entropy if strata_method == "entropy" else stratify_by_boundary + strata_cal = strata_fn(U_cal, n_strata) + strata_test = strata_fn(U_test, n_strata) + sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test) + weights_cal = 1.0 / np.maximum(sigma_cal, 1e-8) + weights_test = 1.0 / np.maximum(sigma_test, 1e-8) + weights_cal /= np.mean(weights_cal) + weights_test /= np.mean(weights_test) + + for m in methods: + start = time.perf_counter() + if m == "global": + res = global_split_conformal(R_cal, R_test, alpha) + elif m == "partition": + res = partition_conformal(R_cal, R_test, alpha, + strata_cal, strata_test) + elif m == "twostage": + res = twostage_conformal(R_cal, R_test, alpha, + U_cal, U_test) + elif m == "jackknife_plus": + res = jackknife_plus_conformal(R_cal, R_test, alpha, U_cal=U_cal, U_test=U_test) + elif m == "weighted": + res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + elif m == "oneshot": + res = oneshot_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "trainres": + train_perm = rng.permutation(n) + idx_train = train_perm[:n_cal] + res = trainres_conformal( + R_cal, R_test, alpha, U_cal, U_test, R[idx_train], U[idx_train] + ) + elif m == "fullcp": + res = full_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "oracle": + res = oracle_conformal(R_cal, R_test, alpha, sigma_cal, sigma_test) + else: + continue + + runtime_sec = time.perf_counter() - start + all_results[m].append(dict( + marginal_coverage=float(marginal_coverage(res.covered)), + max_disparity=float(max_disparity(res.covered, strata_test, alpha)), + worst_stratum_coverage=float(worst_stratum_coverage(res.covered, strata_test)), + mean_radius=float(mean_radius(res.radius)), + sscv=float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), + coverage_variance=float(coverage_variance(res.covered, strata_test)), + runtime_sec=float(runtime_sec), + stratified_coverage={ + str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items() + }, + )) + if compute_volume: + all_results[m][-1]["mean_volume_ratio"] = float( + mean_volume_ratio( + U_test, + res.radius, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ) + ) + all_results[m][-1]["volume_ratio_by_strata"] = { + str(k): float(v) + for k, v in volume_ratio_by_strata( + U_test, + res.radius, + strata_test, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ).items() + } + + if (rep + 1) % 50 == 0: + log.info(f" Rep {rep + 1}/{n_rep}") + + return all_results + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--dataset", choices=["samson", "jasper"], default="samson") + parser.add_argument("--data-dir", default="data/raw/hyperspectral") + parser.add_argument("--unmix", choices=["nnls", "nmf"], default="nnls", + help="Unmixing method: nnls (known endmembers) or nmf (estimated)") + parser.add_argument("--alpha", type=float, default=0.1) + parser.add_argument("--n_rep", type=int, default=200) + parser.add_argument("--cal_frac", type=float, default=0.4) + parser.add_argument("--n_strata", type=int, default=5) + parser.add_argument( + "--strata", + choices=["boundary", "entropy", "dominant", "kmeans", "random"], + default="boundary", + ) + parser.add_argument("--fixed-strata", dest="fixed_strata", action="store_true") + parser.add_argument( + "--separate-strata", + dest="fixed_strata", + action="store_false", + help="Diagnostic only: fit calibration/test strata separately.", + ) + parser.set_defaults(fixed_strata=True) + parser.add_argument( + "--methods", + nargs="+", + default=DEFAULT_METHODS, + choices=DEFAULT_METHODS + ["fullcp"], + ) + parser.add_argument("--tag", default=None) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--output-dir", default="results") + parser.add_argument("--compute-volume", action="store_true") + parser.add_argument("--volume-score", choices=["aitchison", "tv"], default="aitchison") + parser.add_argument("--volume-n-mc", type=int, default=20000) + parser.add_argument("--volume-max-points", type=int, default=None) + args = parser.parse_args() + + # Load data + data = load_hyperspectral(args.data_dir, args.dataset) + + # Unmix + if args.unmix == "nmf": + log.info("Running NMF unmixing (estimated endmembers)...") + U = unmix_nmf(data["pixels"], data["K"], seed=args.seed) + else: + log.info("Running NNLS unmixing (known endmembers)...") + U = unmix_nnls(data["pixels"], data["endmembers"]) + Y = data["abundances"] + + R = aitchison_dist(Y, U) + log.info(f"Residuals: mean={R.mean():.4f}, std={R.std():.4f}, " + f"median={np.median(R):.4f}") + + # Quick quality check + from sklearn.metrics import mean_squared_error + rmse = np.sqrt(mean_squared_error(Y, U)) + corr = np.corrcoef(Y.ravel(), U.ravel())[0, 1] + log.info(f"Unmixing quality: RMSE={rmse:.4f}, Pearson r={corr:.4f}") + + # Check heterogeneity: residuals by dominant endmember + dominant = np.argmax(U, axis=1) + for k in range(data["K"]): + mask = dominant == k + if mask.sum() > 0: + log.info(f" {data['names'][k]:10s}: n={mask.sum():5d}, " + f"R_mean={R[mask].mean():.4f}, R_std={R[mask].std():.4f}") + + # Run experiment + rng = get_rng(args.seed) + log.info(f"\nRunning {args.n_rep} reps, alpha={args.alpha}, " + f"cal_frac={args.cal_frac}...") + + all_results = run_experiment( + Y, U, args.alpha, args.n_rep, args.cal_frac, + args.n_strata, rng, args.methods, + compute_volume=args.compute_volume, + volume_score=args.volume_score, + volume_n_mc=args.volume_n_mc, + volume_max_points=args.volume_max_points, + strata_method=args.strata, + fixed_strata=args.fixed_strata, + strata_seed=args.seed, + ) + + # Aggregate + log.info("\n" + "=" * 60) + log.info(f"RESULTS — Hyperspectral unmixing ({args.dataset})") + log.info("=" * 60) + + summary = {} + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + "mean_volume_ratio", + ] + for m in args.methods: + if not all_results[m]: + continue + reps = all_results[m] + s = {} + for key in scalar_keys: + if key in reps[0]: + vals = [r[key] for r in reps] + s[key] = {"mean": float(np.mean(vals)), "std": float(np.std(vals))} + strata_keys = set() + for r in reps: + strata_keys.update(r["stratified_coverage"].keys()) + s["stratified_coverage"] = { + k: { + "mean": float(np.mean([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "std": float(np.std([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "n_reps": int(sum(k in r["stratified_coverage"] for r in reps)), + } + for k in sorted(strata_keys, key=int) + } + if "volume_ratio_by_strata" in reps[0]: + vol_keys = set() + for r in reps: + vol_keys.update(r["volume_ratio_by_strata"].keys()) + s["volume_ratio_by_strata"] = { + k: { + "mean": float(np.mean([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "std": float(np.std([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "n_reps": int(sum(k in r["volume_ratio_by_strata"] for r in reps)), + } + for k in sorted(vol_keys, key=int) + } + summary[m] = s + log.info( + f" {m:12s} cov={s['marginal_coverage']['mean']:.3f}±{s['marginal_coverage']['std']:.3f} " + f"disp={s['max_disparity']['mean']:.3f}±{s['max_disparity']['std']:.3f} " + f"radius={s['mean_radius']['mean']:.3f}" + ) + + # Save + out_dir = Path(args.output_dir) / "tables" + out_dir.mkdir(parents=True, exist_ok=True) + suffix = f"_{args.unmix}" if args.unmix != "nnls" else "" + tag_suffix = f"_{args.tag}" if args.tag else "" + out_file = out_dir / f"exp2_3_hyperspectral_{args.dataset}{suffix}{tag_suffix}.json" + with open(out_file, "w") as f: + json.dump(dict( + dataset=args.dataset, + summary=summary, + unmixing_rmse=float(rmse), + unmixing_corr=float(corr), + residual_stats=dict(mean=float(R.mean()), std=float(R.std())), + endmember_names=data["names"], + config=vars(args), + raw=all_results, + ), f, indent=2) + log.info(f"\nSaved to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_real_strata_sensitivity.py b/scripts/run_real_strata_sensitivity.py new file mode 100644 index 0000000000000000000000000000000000000000..b02c648d2b858f1693840dc185a33e2cd75f821e --- /dev/null +++ b/scripts/run_real_strata_sensitivity.py @@ -0,0 +1,188 @@ +"""Run stratification-sensitivity sweeps across all real benchmark tasks.""" +from __future__ import annotations + +import argparse +import json +import subprocess +from pathlib import Path + +import yaml + + +CORE_METHODS = ["global", "partition", "twostage", "fullcp", "jackknife_plus"] +ALL_METHODS = [ + "global", + "partition", + "twostage", + "fullcp", + "jackknife_plus", + "oneshot", + "trainres", + "weighted", +] +DEFAULT_STRATA = ["boundary", "entropy", "dominant", "kmeans"] +TASKS = ["cifar10", "topics", "affectivetext", "samson", "utkface", "pbmc"] + + +def build_pbmc_config( + base_config: Path, + out_dir: Path, + strata_method: str, + methods: list[str], +) -> Path: + with open(base_config, encoding="utf-8") as f: + cfg = yaml.safe_load(f) + + cfg["experiment"] = f"pbmc_sensitivity_{base_config.stem}_{strata_method}_fixed" + cfg.setdefault("evaluation", {}) + cfg["evaluation"]["strata_method"] = strata_method + cfg["evaluation"]["fixed_strata"] = True + cfg["conformal"]["methods"] = methods + + out_path = out_dir / f"{cfg['experiment']}.yaml" + with open(out_path, "w", encoding="utf-8") as f: + yaml.safe_dump(cfg, f, sort_keys=False) + return out_path + + +def command_matrix( + mode: str, + strata: list[str], + methods: list[str], + config_dir: Path, +) -> list[list[str]]: + py = ["uv", "run", "--extra", "bio", "python"] + commands: list[list[str]] = [] + + if mode == "smoke": + n_rep = "5" + softmax_extra = ["--max_samples", "2000"] + utk_extra = ["--max_samples", "2000"] + pbmc_base = Path("configs/real/exp2_1_smoke_test.yaml") + else: + n_rep = "50" + softmax_extra = [] + utk_extra = [] + pbmc_base = Path("configs/real/exp2_1_bulk_deconv.yaml") + + method_args = ["--methods", *methods] + + for s in strata: + tag = f"strata_{s}_fixed" + + commands.append([ + *py, "scripts/run_softmax.py", + "--dataset", "cifar10", + "--model", "resnet18", + "--device", "cpu", + "--n_rep", n_rep, + "--n_strata", "5", + "--strata", s, + "--fixed-strata", + "--tag", tag, + *method_args, + *softmax_extra, + ]) + + commands.append([ + *py, "scripts/run_topics.py", + "--K", "10", + "--n_rep", n_rep, + "--n_strata", "5", + "--strata", s, + "--fixed-strata", + "--tag", tag, + *method_args, + ]) + + commands.append([ + *py, "scripts/run_affective_text.py", + "--n_rep", n_rep, + "--n_strata", "5", + "--strata", s, + "--fixed-strata", + "--tag", tag, + *method_args, + ]) + + commands.append([ + *py, "scripts/run_hyperspectral.py", + "--dataset", "samson", + "--unmix", "nmf", + "--n_rep", n_rep, + "--n_strata", "5", + "--strata", s, + "--fixed-strata", + "--tag", tag, + *method_args, + ]) + + commands.append([ + *py, "scripts/run_age_ldl.py", + "--data-dir", "data/raw/UTKFace", + "--pred-method", "image_knn", + "--n_rep", n_rep, + "--n_strata", "5", + "--strata", s, + "--fixed-strata", + "--tag", tag, + *method_args, + *utk_extra, + ]) + + pbmc_cfg = build_pbmc_config(pbmc_base, config_dir, s, methods) + commands.append([ + *py, "scripts/run_bulk_deconv.py", + "--config", str(pbmc_cfg), + ]) + + return commands + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--mode", choices=["smoke", "full"], default="smoke") + parser.add_argument("--method-set", choices=["core", "all"], default="core") + parser.add_argument("--strata", nargs="+", default=DEFAULT_STRATA) + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--continue-on-error", action="store_true") + parser.add_argument("--manifest", default="results/tables/real_strata_sensitivity_manifest.json") + args = parser.parse_args() + + methods = CORE_METHODS if args.method_set == "core" else ALL_METHODS + config_dir = Path("temp/strata_sensitivity_configs") + config_dir.mkdir(parents=True, exist_ok=True) + + commands = command_matrix(args.mode, args.strata, methods, config_dir) + + manifest = { + "mode": args.mode, + "method_set": args.method_set, + "methods": methods, + "strata": args.strata, + "commands": [" ".join(cmd) for cmd in commands], + "completed": [], + "failed": [], + } + + if args.dry_run: + print(json.dumps(manifest, indent=2)) + return + + for i, cmd in enumerate(commands, start=1): + print(f"[{i}/{len(commands)}] {' '.join(cmd)}", flush=True) + try: + subprocess.run(cmd, check=True) + manifest["completed"].append(" ".join(cmd)) + except subprocess.CalledProcessError as exc: + manifest["failed"].append({"command": " ".join(cmd), "returncode": exc.returncode}) + with open(args.manifest, "w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + if not args.continue_on_error: + raise + with open(args.manifest, "w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_single_task.py b/scripts/run_single_task.py new file mode 100644 index 0000000000000000000000000000000000000000..ea130de8cb3183d50a3b54b57012151e938424d8 --- /dev/null +++ b/scripts/run_single_task.py @@ -0,0 +1,20 @@ +import argparse +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TASK_MAP = { + 'synthetic': [sys.executable, str(ROOT / 'scripts' / 'run_synthetic.py')], + 'cifar10': [sys.executable, str(ROOT / 'scripts' / 'run_softmax.py')], + 'topics': [sys.executable, str(ROOT / 'scripts' / 'run_topics.py')], + 'samson': [sys.executable, str(ROOT / 'scripts' / 'run_hyperspectral.py')], + 'pbmc': [sys.executable, str(ROOT / 'scripts' / 'run_bulk_deconv.py')], + 'utkface': [sys.executable, str(ROOT / 'scripts' / 'run_age_ldl.py')], + 'affectivetext': [sys.executable, str(ROOT / 'scripts' / 'run_affective_text.py')], +} +parser = argparse.ArgumentParser() +parser.add_argument('task', choices=TASK_MAP) +parser.add_argument('extra', nargs=argparse.REMAINDER) +args = parser.parse_args() +subprocess.check_call(TASK_MAP[args.task] + args.extra) diff --git a/scripts/run_softmax.py b/scripts/run_softmax.py new file mode 100644 index 0000000000000000000000000000000000000000..777a9f754db3f2bc7cc128bbef84368e5b6bd65b --- /dev/null +++ b/scripts/run_softmax.py @@ -0,0 +1,485 @@ +"""Exp 2.2 — Classification softmax calibration on CIFAR-10/100. + +Softmax output ∈ Δ^{K-1}, one-hot label ∈ Δ^{K-1}. +Tests whether global conformal creates disparity across easy vs hard classes. + +Usage: + python scripts/run_softmax.py --dataset cifar10 + python scripts/run_softmax.py --dataset cifar100 --n_strata 10 +""" +import argparse +import json +import logging +import numpy as np +from pathlib import Path +import time + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.utils.simplex import aitchison_dist +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_boundary, + stratify_by_entropy, +) +from src.utils.seed import get_rng +from src.methods import ( + full_conformal, + global_split_conformal, + jackknife_plus_conformal, + oneshot_conformal, + partition_conformal, + trainres_conformal, + twostage_conformal, + weighted_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from src.metrics.coverage import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from src.metrics.sscv import size_stratified_coverage_violation +from src.metrics.setsize import mean_radius, mean_volume_ratio, volume_ratio_by_strata + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +DEFAULT_METHODS = [ + "global", + "partition", + "twostage", + "jackknife_plus", + "weighted", + "oneshot", + "trainres", +] + + +def get_softmax_predictions(dataset: str, model_name: str = "resnet50", + device: str = "cuda"): + """Train or load a classifier, return softmax predictions on test set. + + Returns: + Y: one-hot labels (n, K) + U: softmax predictions (n, K) + class_names: list of class names + """ + # Check for cached predictions + cache_path = Path(f"data/processed/{dataset}_{model_name}_softmax.npz") + if cache_path.exists(): + log.info(f"Loading cached predictions from {cache_path}") + data = np.load(cache_path) + return data["Y"], data["U"], list(data["class_names"]) + + import torch + import torch.nn as nn + import torchvision + import torchvision.transforms as T + + # Load dataset + if dataset == "cifar10": + transform = T.Compose([T.Resize(224), T.ToTensor(), + T.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) + testset = torchvision.datasets.CIFAR10( + root="data/raw", train=False, download=True, transform=transform) + trainset = torchvision.datasets.CIFAR10( + root="data/raw", train=True, download=True, transform=transform) + K = 10 + class_names = testset.classes + elif dataset == "cifar100": + transform = T.Compose([T.Resize(224), T.ToTensor(), + T.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) + testset = torchvision.datasets.CIFAR100( + root="data/raw", train=False, download=True, transform=transform) + trainset = torchvision.datasets.CIFAR100( + root="data/raw", train=True, download=True, transform=transform) + K = 100 + class_names = testset.classes + else: + raise ValueError(f"Unknown dataset: {dataset}") + + log.info(f"Training/loading {model_name} on {dataset}...") + + # Use pretrained model + finetune last layer + if model_name == "resnet50": + model = torchvision.models.resnet50(weights="IMAGENET1K_V1") + model.fc = nn.Linear(model.fc.in_features, K) + elif model_name == "resnet18": + model = torchvision.models.resnet18(weights="IMAGENET1K_V1") + model.fc = nn.Linear(model.fc.in_features, K) + else: + raise ValueError(f"Unknown model: {model_name}") + + model = model.to(device) + + # Quick finetune (5 epochs, enough for reasonable softmax) + trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, + shuffle=True, num_workers=4) + optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) + criterion = nn.CrossEntropyLoss() + + model.train() + for epoch in range(5): + total_loss = 0 + for images, labels in trainloader: + images, labels = images.to(device), labels.to(device) + outputs = model(images) + loss = criterion(outputs, labels) + optimizer.zero_grad() + loss.backward() + optimizer.step() + total_loss += loss.item() + log.info(f" Epoch {epoch+1}/5, loss={total_loss/len(trainloader):.4f}") + + # Get test predictions + model.eval() + testloader = torch.utils.data.DataLoader(testset, batch_size=256, + shuffle=False, num_workers=4) + all_probs = [] + all_labels = [] + + with torch.no_grad(): + for images, labels in testloader: + images = images.to(device) + outputs = model(images) + probs = torch.softmax(outputs, dim=1).cpu().numpy() + all_probs.append(probs) + all_labels.append(labels.numpy()) + + U = np.concatenate(all_probs) # (n, K) softmax predictions + labels = np.concatenate(all_labels) # (n,) integer labels + + # One-hot encode labels (these are vertices of the simplex) + Y = np.zeros((len(labels), K)) + Y[np.arange(len(labels)), labels] = 1.0 + # Add tiny smoothing to avoid log(0) in Aitchison distance + Y = (Y + 1e-8) + Y = Y / Y.sum(axis=1, keepdims=True) + + acc = (np.argmax(U, axis=1) == labels).mean() + log.info(f"Test accuracy: {acc:.4f}") + + # Cache + cache_path.parent.mkdir(parents=True, exist_ok=True) + np.savez(cache_path, Y=Y, U=U, class_names=np.array(class_names)) + log.info(f"Cached predictions to {cache_path}") + + return Y, U, class_names + + +def compute_weight_vectors(R_cal, U_cal, U_test, k=20): + sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal, k=k) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=k) + weights_cal = 1.0 / np.maximum(sigma_cal, 1e-8) + weights_test = 1.0 / np.maximum(sigma_test, 1e-8) + weights_cal /= np.mean(weights_cal) + weights_test /= np.mean(weights_test) + return weights_cal, weights_test + + +def evaluate_result( + res, + U_test, + strata_test, + alpha, + runtime_sec, + compute_volume=False, + volume_score="tv", + volume_n_mc=50000, + volume_max_points=None, + rep=0, +): + result = dict( + marginal_coverage=float(marginal_coverage(res.covered)), + max_disparity=float(max_disparity(res.covered, strata_test, alpha)), + worst_stratum_coverage=float(worst_stratum_coverage(res.covered, strata_test)), + mean_radius=float(mean_radius(res.radius)), + sscv=float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), + coverage_variance=float(coverage_variance(res.covered, strata_test)), + runtime_sec=float(runtime_sec), + stratified_coverage={ + str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items() + }, + ) + if compute_volume: + result["mean_volume_ratio"] = float( + mean_volume_ratio( + U_test, + res.radius, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ) + ) + result["volume_ratio_by_strata"] = { + str(k): float(v) + for k, v in volume_ratio_by_strata( + U_test, + res.radius, + strata_test, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ).items() + } + return result + + +def run_experiment( + Y, + U, + alpha, + n_rep, + cal_frac, + n_strata, + rng, + methods, + compute_volume=False, + volume_score="tv", + volume_n_mc=50000, + volume_max_points=None, + strata_method="entropy", + fixed_strata=True, + strata_seed=2026, +): + """Run conformal with repeated splits.""" + # Use L1 distance instead of Aitchison for one-hot labels + # (Aitchison is ill-defined at simplex vertices) + R = np.sum(np.abs(Y - U), axis=1) / 2.0 # total variation distance + + n = len(R) + n_cal = int(n * cal_frac) + + all_results = {m: [] for m in methods} + fixed_labels = None + if fixed_strata: + fixed_labels = precompute_fixed_strata(U, strata_method, n_strata, seed=strata_seed) + elif strata_method not in {"boundary", "entropy"}: + raise ValueError("Non-fixed softmax strata must be 'boundary' or 'entropy'.") + + for rep in range(n_rep): + perm = rng.permutation(n) + idx_cal, idx_test = perm[:n_cal], perm[n_cal:] + + R_cal, R_test = R[idx_cal], R[idx_test] + U_cal, U_test = U[idx_cal], U[idx_test] + + if fixed_labels is not None: + strata_cal = fixed_labels[idx_cal] + strata_test = fixed_labels[idx_test] + else: + strata_fn = stratify_by_boundary if strata_method == "boundary" else stratify_by_entropy + strata_cal = strata_fn(U_cal, n_strata) + strata_test = strata_fn(U_test, n_strata) + weights_cal, weights_test = compute_weight_vectors(R_cal, U_cal, U_test) + + for m in methods: + start = time.perf_counter() + if m == "global": + res = global_split_conformal(R_cal, R_test, alpha) + elif m == "partition": + res = partition_conformal(R_cal, R_test, alpha, + strata_cal, strata_test) + elif m == "twostage": + res = twostage_conformal(R_cal, R_test, alpha, + U_cal, U_test) + elif m == "jackknife_plus": + res = jackknife_plus_conformal(R_cal, R_test, alpha, U_cal=U_cal, U_test=U_test) + elif m == "weighted": + res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + elif m == "oneshot": + res = oneshot_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "trainres": + train_perm = rng.permutation(n) + idx_train = train_perm[:n_cal] + res = trainres_conformal( + R_cal, R_test, alpha, U_cal, U_test, R[idx_train], U[idx_train] + ) + elif m == "fullcp": + res = full_conformal(R_cal, R_test, alpha, U_cal, U_test) + else: + continue + + runtime_sec = time.perf_counter() - start + all_results[m].append( + evaluate_result( + res, + U_test, + strata_test, + alpha, + runtime_sec, + compute_volume=compute_volume, + volume_score=volume_score, + volume_n_mc=volume_n_mc, + volume_max_points=volume_max_points, + rep=rep, + ) + ) + + if (rep + 1) % 50 == 0: + log.info(f" Rep {rep + 1}/{n_rep}") + + return all_results + + +def maybe_subsample(Y, U, max_samples, rng): + if max_samples is None or max_samples >= len(Y): + return Y, U + idx = rng.choice(len(Y), size=max_samples, replace=False) + return Y[idx], U[idx] + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--dataset", default="cifar10", choices=["cifar10", "cifar100"]) + parser.add_argument("--model", default="resnet18") + parser.add_argument("--device", default="cuda") + parser.add_argument("--alpha", type=float, default=0.1) + parser.add_argument("--n_rep", type=int, default=200) + parser.add_argument("--cal_frac", type=float, default=0.4) + parser.add_argument("--n_strata", type=int, default=5) + parser.add_argument( + "--strata", + choices=["entropy", "boundary", "dominant", "kmeans", "random"], + default="entropy", + ) + parser.add_argument("--fixed-strata", dest="fixed_strata", action="store_true") + parser.add_argument( + "--separate-strata", + dest="fixed_strata", + action="store_false", + help="Diagnostic only: fit calibration/test strata separately.", + ) + parser.set_defaults(fixed_strata=True) + parser.add_argument("--max_samples", type=int, default=None) + parser.add_argument("--compute-volume", action="store_true") + parser.add_argument("--volume-score", choices=["tv", "aitchison"], default="tv") + parser.add_argument("--volume-n-mc", type=int, default=50000) + parser.add_argument("--volume-max-points", type=int, default=None) + parser.add_argument( + "--methods", + nargs="+", + default=DEFAULT_METHODS, + choices=DEFAULT_METHODS + ["fullcp"], + ) + parser.add_argument("--tag", default=None) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--output-dir", default="results") + args = parser.parse_args() + + rng = get_rng(args.seed) + + # Get predictions + Y, U, class_names = get_softmax_predictions(args.dataset, args.model, args.device) + Y, U = maybe_subsample(Y, U, args.max_samples, rng) + K = Y.shape[1] + log.info(f"Dataset: {args.dataset}, K={K}, n={len(Y)}") + + # Residual diagnostics + R = np.sum(np.abs(Y - U), axis=1) / 2.0 + log.info(f"Residuals: mean={R.mean():.4f}, std={R.std():.4f}") + + # Per-class residuals + true_labels = np.argmax(Y, axis=1) + for k in range(min(K, 10)): + mask = true_labels == k + log.info(f" {class_names[k]:12s}: n={mask.sum()}, " + f"R_mean={R[mask].mean():.4f}, R_std={R[mask].std():.4f}") + + # Run + all_results = run_experiment( + Y, + U, + args.alpha, + args.n_rep, + args.cal_frac, + args.n_strata, + rng, + args.methods, + compute_volume=args.compute_volume, + volume_score=args.volume_score, + volume_n_mc=args.volume_n_mc, + volume_max_points=args.volume_max_points, + strata_method=args.strata, + fixed_strata=args.fixed_strata, + strata_seed=args.seed, + ) + + # Aggregate + log.info("\n" + "=" * 60) + log.info(f"RESULTS — Softmax calibration ({args.dataset})") + log.info("=" * 60) + + summary = {} + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + "mean_volume_ratio", + ] + for m in args.methods: + if not all_results[m]: + continue + reps = all_results[m] + s = {} + for key in scalar_keys: + if key in reps[0]: + vals = [r[key] for r in reps] + s[key] = {"mean": float(np.mean(vals)), "std": float(np.std(vals))} + strata_keys = set() + for r in reps: + strata_keys.update(r["stratified_coverage"].keys()) + s["stratified_coverage"] = { + k: { + "mean": float(np.mean([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "std": float(np.std([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "n_reps": int(sum(k in r["stratified_coverage"] for r in reps)), + } + for k in sorted(strata_keys, key=int) + } + if "volume_ratio_by_strata" in reps[0]: + vol_keys = set() + for r in reps: + vol_keys.update(r["volume_ratio_by_strata"].keys()) + s["volume_ratio_by_strata"] = { + k: { + "mean": float(np.mean([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "std": float(np.std([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "n_reps": int(sum(k in r["volume_ratio_by_strata"] for r in reps)), + } + for k in sorted(vol_keys, key=int) + } + summary[m] = s + log.info( + f" {m:12s} cov={s['marginal_coverage']['mean']:.3f}±{s['marginal_coverage']['std']:.3f} " + f"disp={s['max_disparity']['mean']:.3f}±{s['max_disparity']['std']:.3f} " + f"worst={s['worst_stratum_coverage']['mean']:.3f} " + f"sscv={s['sscv']['mean']:.3f}" + ) + + # Save + out_dir = Path(args.output_dir) / "tables" + out_dir.mkdir(parents=True, exist_ok=True) + suffix = f"_{args.tag}" if args.tag else "" + out_file = out_dir / f"exp2_2_softmax_{args.dataset}{suffix}.json" + with open(out_file, "w") as f: + json.dump(dict(summary=summary, dataset=args.dataset, K=K, + class_names=class_names, config=vars(args), raw=all_results), + f, indent=2) + log.info(f"Saved to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_synthetic.py b/scripts/run_synthetic.py new file mode 100644 index 0000000000000000000000000000000000000000..5231f554cd4bcedafd80d51720e84194474c5233 --- /dev/null +++ b/scripts/run_synthetic.py @@ -0,0 +1,455 @@ +"""Run a synthetic SimplexUQ benchmark experiment from config.""" +import argparse +import json +import logging +from pathlib import Path +import sys +import time + +import numpy as np +import yaml + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.dgp.discrete_groups import DiscreteGroupsDGP +from src.dgp.heavy_tail import HeavyTailDGP +from src.dgp.high_k import HighKDGP +from src.dgp.model_bias import ModelBiasDGP +from src.dgp.pure_scale import PureScaleDGP +from src.methods import ( + full_conformal, + global_split_conformal, + jackknife_plus_conformal, + oneshot_conformal, + oracle_conformal, + partition_conformal, + trainres_conformal, + twostage_conformal, + weighted_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from src.metrics import ( + coverage_variance, + marginal_coverage, + max_disparity, + mean_radius, + mean_volume_ratio, + size_stratified_coverage_violation, + stratified_coverage, + volume_ratio_by_strata, + worst_stratum_coverage, +) +from src.utils.seed import get_rng +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_argmax_group, + stratify_by_boundary, + stratify_by_entropy, + stratify_by_kmeans, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +DGP_MAP = { + "pure_scale": PureScaleDGP, + "model_bias": ModelBiasDGP, + "discrete_groups": DiscreteGroupsDGP, + "heavy_tail": HeavyTailDGP, + "high_k": HighKDGP, +} + +STRATA_MAP = { + "boundary": stratify_by_boundary, + "entropy": stratify_by_entropy, + "kmeans": stratify_by_kmeans, +} + +DEFAULT_METHODS = [ + "global", + "fullcp", + "jackknife_plus", + "partition", + "twostage", + "oneshot", + "trainres", + "weighted", + "oracle", +] + + +def get_strata(U: np.ndarray, cfg: dict) -> np.ndarray: + method = cfg["evaluation"]["strata_method"] + if method == "argmax_group": + split_index = cfg["evaluation"].get("split_index", cfg["dgp"].get("easy_classes", 5)) + return stratify_by_argmax_group(U, split_index=split_index) + return STRATA_MAP[method](U, cfg["evaluation"]["n_strata"]) + + +def get_fixed_strata_labels(U: np.ndarray, cfg: dict) -> np.ndarray: + """Compute one prediction-space stratification map before cal/test splitting.""" + method = cfg["evaluation"]["strata_method"] + if method == "argmax_group": + split_index = cfg["evaluation"].get("split_index", cfg["dgp"].get("easy_classes", 5)) + return stratify_by_argmax_group(U, split_index=split_index) + return precompute_fixed_strata( + U, + method, + cfg["evaluation"]["n_strata"], + seed=cfg.get("seed", 2026), + ) + + +def get_alpha(cfg: dict) -> float: + if "conformal" in cfg: + return cfg["conformal"]["alpha"] + return cfg["evaluation"]["alpha"] + + +def get_methods(cfg: dict) -> list[str]: + if "conformal" in cfg and "methods" in cfg["conformal"]: + return list(cfg["conformal"]["methods"]) + return list(cfg.get("methods", DEFAULT_METHODS)) + + +def get_method_params(cfg: dict, method_name: str) -> dict: + return dict(cfg.get("method_params", {}).get(method_name, {})) + + +def compute_weight_vectors( + cfg: dict, + R_cal: np.ndarray, + U_cal: np.ndarray, + U_test: np.ndarray, + sigma_cal_true: np.ndarray | None, + sigma_test_true: np.ndarray | None, +) -> tuple[np.ndarray, np.ndarray]: + """Build weighted-CP importance weights. + + Defaults to inverse local scale so that hard regions receive more mass. + """ + weight_cfg = cfg.get("weighting", {}) + mode = weight_cfg.get("mode", "inverse_sigma") + source = weight_cfg.get("source", "knn") + eps = float(weight_cfg.get("eps", 1e-8)) + + if mode != "inverse_sigma": + raise ValueError(f"Unsupported weighting mode: {mode}") + + if source == "oracle" and sigma_cal_true is not None and sigma_test_true is not None: + sigma_cal = sigma_cal_true + sigma_test = sigma_test_true + elif source == "knn_loo": + sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal, k=weight_cfg.get("k", 20)) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=weight_cfg.get("k", 20)) + else: + sigma_cal = knn_sigma_hat(U_cal, R_cal, U_cal, k=weight_cfg.get("k", 20)) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=weight_cfg.get("k", 20)) + + weights_cal = 1.0 / np.maximum(sigma_cal, eps) + weights_test = 1.0 / np.maximum(sigma_test, eps) + + # Weighted conformal depends only on relative weights; normalize for stability. + weights_cal = weights_cal / np.mean(weights_cal) + weights_test = weights_test / np.mean(weights_test) + return weights_cal, weights_test + + +def evaluate_method( + res, + U_test: np.ndarray, + strata_test: np.ndarray, + alpha: float, + runtime_sec: float, + cfg: dict, +) -> dict: + metrics = { + "marginal_coverage": float(marginal_coverage(res.covered)), + "max_disparity": float(max_disparity(res.covered, strata_test, alpha)), + "worst_stratum_coverage": float(worst_stratum_coverage(res.covered, strata_test)), + "mean_radius": float(mean_radius(res.radius)), + "sscv": float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), + "coverage_variance": float(coverage_variance(res.covered, strata_test)), + "runtime_sec": float(runtime_sec), + "stratified_coverage": { + str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items() + }, + } + volume_cfg = cfg["evaluation"].get("volume", {}) + if volume_cfg.get("compute", False): + score = volume_cfg.get("score", "aitchison") + n_mc = int(volume_cfg.get("n_mc", 20000)) + max_points = volume_cfg.get("max_points") + seed = int(volume_cfg.get("seed", 0)) + metrics["mean_volume_ratio"] = float( + mean_volume_ratio( + U_test, + res.radius, + score=score, + n_mc=n_mc, + max_points=max_points, + rng=np.random.default_rng(seed), + ) + ) + metrics["volume_ratio_by_strata"] = { + str(k): float(v) + for k, v in volume_ratio_by_strata( + U_test, + res.radius, + strata_test, + score=score, + n_mc=n_mc, + max_points=max_points, + rng=np.random.default_rng(seed), + ).items() + } + return metrics + + +def run_one_rep(dgp, cfg: dict, rng: np.random.Generator) -> dict: + """Generate one synthetic draw and evaluate the requested methods.""" + dcfg = cfg["data"] + ecfg = cfg["evaluation"] + alpha = get_alpha(cfg) + + n_total = dcfg["n_cal"] + dcfg["n_test"] + sample = dgp.sample(n_total, rng) + + idx = rng.permutation(n_total) + idx_cal = idx[:dcfg["n_cal"]] + idx_test = idx[dcfg["n_cal"]:] + + R_cal, R_test = sample.R[idx_cal], sample.R[idx_test] + U_cal, U_test = sample.U[idx_cal], sample.U[idx_test] + sigma_cal = sample.sigma_true[idx_cal] if sample.sigma_true is not None else None + sigma_test = sample.sigma_true[idx_test] if sample.sigma_true is not None else None + + fixed_strata = get_fixed_strata_labels(sample.U, cfg) + strata_cal = fixed_strata[idx_cal] + strata_test = fixed_strata[idx_test] + weights_cal, weights_test = compute_weight_vectors( + cfg, R_cal, U_cal, U_test, sigma_cal, sigma_test + ) + + results = {} + methods = get_methods(cfg) + + for method_name in methods: + method_params = get_method_params(cfg, method_name) + start = time.perf_counter() + + if method_name == "global": + res = global_split_conformal(R_cal, R_test, alpha) + elif method_name == "partition": + res = partition_conformal(R_cal, R_test, alpha, strata_cal, strata_test) + elif method_name == "twostage": + res = twostage_conformal( + R_cal, + R_test, + alpha, + U_cal, + U_test, + n_scale_est=dcfg.get("n_scale_est"), + k=method_params.get("k", 20), + ) + elif method_name == "fullcp": + res = full_conformal( + R_cal, + R_test, + alpha, + U_cal, + U_test, + k=method_params.get("k", 20), + ) + elif method_name == "jackknife_plus": + res = jackknife_plus_conformal( + R_cal, + R_test, + alpha, + U_cal=U_cal, + U_test=U_test, + loo_scores=None, + k=method_params.get("k", 20), + ) + elif method_name == "oracle": + if sigma_cal is None or sigma_test is None: + log.warning("Skipping oracle: true sigma unavailable") + continue + res = oracle_conformal(R_cal, R_test, alpha, sigma_cal, sigma_test) + elif method_name == "oneshot": + res = oneshot_conformal( + R_cal, + R_test, + alpha, + U_cal, + U_test, + k=method_params.get("k", 20), + ) + elif method_name == "trainres": + n_train = dcfg.get("n_train", dcfg["n_cal"]) + train_sample = dgp.sample(n_train, rng) + res = trainres_conformal( + R_cal, + R_test, + alpha, + U_cal, + U_test, + train_sample.R, + train_sample.U, + k=method_params.get("k", 20), + ) + elif method_name == "weighted": + res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + else: + log.warning("Unknown method: %s", method_name) + continue + + runtime_sec = time.perf_counter() - start + results[method_name] = evaluate_method( + res, + U_test, + strata_test, + alpha, + runtime_sec=runtime_sec, + cfg=cfg, + ) + + return results + + +def aggregate_repetitions(all_results: dict[str, list[dict]]) -> dict: + """Aggregate scalar and per-stratum metrics over repetitions.""" + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + "mean_volume_ratio", + ] + + summary = {} + for method_name, reps in all_results.items(): + if not reps: + continue + + summary[method_name] = {} + for key in scalar_keys: + if key in reps[0]: + values = [rep[key] for rep in reps] + summary[method_name][key] = { + "mean": float(np.mean(values)), + "std": float(np.std(values)), + } + + all_strata_keys = set() + for rep in reps: + all_strata_keys.update(rep["stratified_coverage"].keys()) + + summary[method_name]["stratified_coverage"] = {} + for stratum in sorted(all_strata_keys, key=int): + values = [ + rep["stratified_coverage"][stratum] + for rep in reps + if stratum in rep["stratified_coverage"] + ] + summary[method_name]["stratified_coverage"][stratum] = { + "mean": float(np.mean(values)), + "std": float(np.std(values)), + "n_reps": len(values), + } + + if "volume_ratio_by_strata" in reps[0]: + all_vol_keys = set() + for rep in reps: + all_vol_keys.update(rep["volume_ratio_by_strata"].keys()) + summary[method_name]["volume_ratio_by_strata"] = {} + for stratum in sorted(all_vol_keys, key=int): + values = [ + rep["volume_ratio_by_strata"][stratum] + for rep in reps + if stratum in rep["volume_ratio_by_strata"] + ] + summary[method_name]["volume_ratio_by_strata"][stratum] = { + "mean": float(np.mean(values)), + "std": float(np.std(values)), + "n_reps": len(values), + } + + return summary + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + args = parser.parse_args() + + with open(args.config) as f: + cfg = yaml.safe_load(f) + + log.info("Experiment: %s", cfg["experiment"]) + + dgp_cfg = cfg["dgp"] + dgp_name = dgp_cfg["name"] + dgp_cls = DGP_MAP[dgp_name] + dgp_kwargs = {k: v for k, v in dgp_cfg.items() if k != "name"} + dgp = dgp_cls(**dgp_kwargs) + + n_rep = cfg["data"]["n_rep"] + methods = get_methods(cfg) + rng = get_rng(cfg["seed"]) + all_results = {method: [] for method in methods} + + for rep_idx in range(n_rep): + rep_results = run_one_rep(dgp, cfg, rng) + for method_name, metrics in rep_results.items(): + all_results[method_name].append(metrics) + + if (rep_idx + 1) % 25 == 0 or rep_idx == n_rep - 1: + log.info(" Completed %d/%d reps", rep_idx + 1, n_rep) + + summary = aggregate_repetitions(all_results) + + log.info("") + log.info("=" * 72) + log.info("RESULTS (mean +- std over %d reps)", n_rep) + log.info("=" * 72) + for method_name in methods: + if method_name not in summary: + continue + cov = summary[method_name]["marginal_coverage"]["mean"] + disp = summary[method_name]["max_disparity"]["mean"] + worst = summary[method_name]["worst_stratum_coverage"]["mean"] + radius = summary[method_name]["mean_radius"]["mean"] + sscv = summary[method_name]["sscv"]["mean"] + log.info( + " %-14s cov=%.3f disp=%.3f worst=%.3f radius=%.3f sscv=%.3f", + method_name, + cov, + disp, + worst, + radius, + sscv, + ) + + out_dir = Path("results/tables") + out_dir.mkdir(parents=True, exist_ok=True) + out_file = out_dir / f"{cfg['experiment']}.json" + with open(out_file, "w") as f: + json.dump( + { + "config": cfg, + "methods": methods, + "summary": summary, + "raw": all_results, + }, + f, + indent=2, + ) + log.info("Saved to %s", out_file) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_topics.py b/scripts/run_topics.py new file mode 100644 index 0000000000000000000000000000000000000000..75864dc25a3d7f087a2ec429cc1ce006acef19e6 --- /dev/null +++ b/scripts/run_topics.py @@ -0,0 +1,401 @@ +"""Exp 2.5 — Topic proportion prediction on 20 Newsgroups. + +Train LDA to get topic proportions (ground truth), then predict from TF-IDF features. +Output ∈ Δ^{K-1} where K = number of topics. + +No external data download needed — sklearn has 20 Newsgroups built in. + +Usage: + python scripts/run_topics.py --K 10 + python scripts/run_topics.py --K 20 +""" +import argparse +import json +import logging +import numpy as np +from pathlib import Path +import time + +import sys +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.utils.simplex import aitchison_dist +from src.utils.strata import ( + precompute_fixed_strata, + stratify_by_boundary, + stratify_by_entropy, +) +from src.utils.seed import get_rng +from src.methods import ( + full_conformal, + global_split_conformal, + jackknife_plus_conformal, + oneshot_conformal, + partition_conformal, + trainres_conformal, + twostage_conformal, + weighted_conformal, +) +from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from src.metrics.coverage import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from src.metrics.sscv import size_stratified_coverage_violation +from src.metrics.setsize import mean_radius, mean_volume_ratio, volume_ratio_by_strata + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + +DEFAULT_METHODS = [ + "global", + "partition", + "twostage", + "jackknife_plus", + "weighted", + "oneshot", + "trainres", +] + + +def prepare_topic_data(K: int = 10, n_features: int = 5000, seed: int = 2026): + """Build topic proportion prediction task from 20 Newsgroups. + + Pipeline: + 1. Load 20 Newsgroups, compute TF-IDF + 2. Fit LDA with K topics -> get document-topic proportions (ground truth Y) + 3. Train a regression model from TF-IDF -> topic proportions (predictions U) + + Returns: + Y: ground truth topic proportions (n, K) + U: predicted topic proportions (n, K) + X_tfidf: TF-IDF features (n, n_features) + """ + from sklearn.datasets import fetch_20newsgroups + from sklearn.feature_extraction.text import TfidfVectorizer + from sklearn.decomposition import LatentDirichletAllocation + from sklearn.feature_extraction.text import CountVectorizer + from sklearn.neighbors import KNeighborsRegressor + from sklearn.model_selection import train_test_split + + rng = np.random.default_rng(seed) + + # Load data + log.info("Loading 20 Newsgroups...") + newsgroups = fetch_20newsgroups(subset="all", remove=("headers", "footers", "quotes")) + texts = newsgroups.data + log.info(f" {len(texts)} documents") + + # Count vectorizer for LDA + log.info("Fitting count vectorizer...") + count_vec = CountVectorizer(max_df=0.95, min_df=2, + max_features=n_features, stop_words="english") + X_counts = count_vec.fit_transform(texts) + + # TF-IDF for prediction features + log.info("Computing TF-IDF features...") + tfidf_vec = TfidfVectorizer(max_df=0.95, min_df=2, + max_features=n_features, stop_words="english") + X_tfidf = tfidf_vec.fit_transform(texts) + + # Fit LDA -> ground truth proportions + log.info(f"Fitting LDA with K={K} topics...") + lda = LatentDirichletAllocation( + n_components=K, random_state=seed, max_iter=20, + learning_method="online", batch_size=256, + ) + Y = lda.fit_transform(X_counts) # (n, K), rows sum to 1 + # Ensure simplex + Y = np.maximum(Y, 1e-8) + Y = Y / Y.sum(axis=1, keepdims=True) + + log.info(f" Topic proportions: shape={Y.shape}, " + f"entropy range=[{(-Y * np.log(Y)).sum(1).min():.2f}, " + f"{(-Y * np.log(Y)).sum(1).max():.2f}]") + + # Predict topic proportions from TF-IDF using kNN + log.info("Training kNN predictor for topic proportions...") + n = len(texts) + train_idx = rng.choice(n, size=int(0.6 * n), replace=False) + test_mask = np.ones(n, dtype=bool) + test_mask[train_idx] = False + + X_dense = X_tfidf.toarray() + + # Use PCA to reduce dimensionality for kNN + from sklearn.decomposition import TruncatedSVD + svd = TruncatedSVD(n_components=100, random_state=seed) + X_reduced = svd.fit_transform(X_tfidf) + + knn = KNeighborsRegressor(n_neighbors=30, weights="distance", n_jobs=-1) + knn.fit(X_reduced[train_idx], Y[train_idx]) + U = knn.predict(X_reduced) + U = np.maximum(U, 1e-8) + U = U / U.sum(axis=1, keepdims=True) + + # Only use test portion for evaluation + Y_eval = Y[test_mask] + U_eval = U[test_mask] + + log.info(f" Evaluation set: {len(Y_eval)} documents") + + return Y_eval, U_eval + + +def compute_weight_vectors(R_cal, U_cal, U_test, k=20): + sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal, k=k) + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=k) + weights_cal = 1.0 / np.maximum(sigma_cal, 1e-8) + weights_test = 1.0 / np.maximum(sigma_test, 1e-8) + weights_cal /= np.mean(weights_cal) + weights_test /= np.mean(weights_test) + return weights_cal, weights_test + + +def run_experiment( + Y, + U, + alpha, + n_rep, + cal_frac, + n_strata, + rng, + methods, + compute_volume=False, + volume_score="aitchison", + volume_n_mc=50000, + volume_max_points=None, + strata_method="entropy", + fixed_strata=True, + strata_seed=2026, +): + """Standard conformal experiment.""" + R = aitchison_dist(Y, U) + n = len(R) + n_cal = int(n * cal_frac) + + all_results = {m: [] for m in methods} + fixed_labels = None + if fixed_strata: + fixed_labels = precompute_fixed_strata(U, strata_method, n_strata, seed=strata_seed) + elif strata_method not in {"boundary", "entropy"}: + raise ValueError("Non-fixed topic strata must be 'boundary' or 'entropy'.") + + for rep in range(n_rep): + perm = rng.permutation(n) + idx_cal, idx_test = perm[:n_cal], perm[n_cal:] + + R_cal, R_test = R[idx_cal], R[idx_test] + U_cal, U_test = U[idx_cal], U[idx_test] + + if fixed_labels is not None: + strata_cal = fixed_labels[idx_cal] + strata_test = fixed_labels[idx_test] + else: + strata_fn = stratify_by_boundary if strata_method == "boundary" else stratify_by_entropy + strata_cal = strata_fn(U_cal, n_strata) + strata_test = strata_fn(U_test, n_strata) + weights_cal, weights_test = compute_weight_vectors(R_cal, U_cal, U_test) + + for m in methods: + start = time.perf_counter() + if m == "global": + res = global_split_conformal(R_cal, R_test, alpha) + elif m == "partition": + res = partition_conformal(R_cal, R_test, alpha, + strata_cal, strata_test) + elif m == "twostage": + res = twostage_conformal(R_cal, R_test, alpha, + U_cal, U_test) + elif m == "jackknife_plus": + res = jackknife_plus_conformal(R_cal, R_test, alpha, U_cal=U_cal, U_test=U_test) + elif m == "weighted": + res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) + elif m == "oneshot": + res = oneshot_conformal(R_cal, R_test, alpha, U_cal, U_test) + elif m == "trainres": + train_perm = rng.permutation(n) + idx_train = train_perm[:n_cal] + res = trainres_conformal( + R_cal, R_test, alpha, U_cal, U_test, R[idx_train], U[idx_train] + ) + elif m == "fullcp": + res = full_conformal(R_cal, R_test, alpha, U_cal, U_test) + else: + continue + + runtime_sec = time.perf_counter() - start + all_results[m].append(dict( + marginal_coverage=float(marginal_coverage(res.covered)), + max_disparity=float(max_disparity(res.covered, strata_test, alpha)), + worst_stratum_coverage=float(worst_stratum_coverage(res.covered, strata_test)), + mean_radius=float(mean_radius(res.radius)), + sscv=float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), + coverage_variance=float(coverage_variance(res.covered, strata_test)), + runtime_sec=float(runtime_sec), + stratified_coverage={ + str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items() + }, + )) + if compute_volume: + all_results[m][-1]["mean_volume_ratio"] = float( + mean_volume_ratio( + U_test, + res.radius, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ) + ) + all_results[m][-1]["volume_ratio_by_strata"] = { + str(k): float(v) + for k, v in volume_ratio_by_strata( + U_test, + res.radius, + strata_test, + score=volume_score, + n_mc=volume_n_mc, + max_points=volume_max_points, + rng=np.random.default_rng(rep), + ).items() + } + + if (rep + 1) % 50 == 0: + log.info(f" Rep {rep + 1}/{n_rep}") + + return all_results + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--K", type=int, default=10, help="Number of LDA topics") + parser.add_argument("--alpha", type=float, default=0.1) + parser.add_argument("--n_rep", type=int, default=200) + parser.add_argument("--cal_frac", type=float, default=0.4) + parser.add_argument("--n_strata", type=int, default=5) + parser.add_argument( + "--strata", + choices=["entropy", "boundary", "dominant", "kmeans", "random"], + default="entropy", + ) + parser.add_argument("--fixed-strata", dest="fixed_strata", action="store_true") + parser.add_argument( + "--separate-strata", + dest="fixed_strata", + action="store_false", + help="Diagnostic only: fit calibration/test strata separately.", + ) + parser.set_defaults(fixed_strata=True) + parser.add_argument( + "--methods", + nargs="+", + default=DEFAULT_METHODS, + choices=DEFAULT_METHODS + ["fullcp"], + ) + parser.add_argument("--tag", default=None) + parser.add_argument("--seed", type=int, default=2026) + parser.add_argument("--output-dir", default="results") + parser.add_argument("--compute-volume", action="store_true") + parser.add_argument("--volume-score", choices=["aitchison", "tv"], default="aitchison") + parser.add_argument("--volume-n-mc", type=int, default=50000) + parser.add_argument("--volume-max-points", type=int, default=None) + args = parser.parse_args() + + rng = get_rng(args.seed) + + # Prepare data + Y, U = prepare_topic_data(K=args.K, seed=args.seed) + K = Y.shape[1] + + R = aitchison_dist(Y, U) + log.info(f"Residuals: mean={R.mean():.4f}, std={R.std():.4f}") + + # Run + all_results = run_experiment( + Y, + U, + args.alpha, + args.n_rep, + args.cal_frac, + args.n_strata, + rng, + args.methods, + compute_volume=args.compute_volume, + volume_score=args.volume_score, + volume_n_mc=args.volume_n_mc, + volume_max_points=args.volume_max_points, + strata_method=args.strata, + fixed_strata=args.fixed_strata, + strata_seed=args.seed, + ) + + # Report + log.info("\n" + "=" * 60) + log.info(f"RESULTS — Topic proportions (K={K})") + log.info("=" * 60) + + summary = {} + scalar_keys = [ + "marginal_coverage", + "max_disparity", + "worst_stratum_coverage", + "mean_radius", + "sscv", + "coverage_variance", + "runtime_sec", + "mean_volume_ratio", + ] + for m in args.methods: + if not all_results[m]: + continue + reps = all_results[m] + s = {} + for key in scalar_keys: + if key in reps[0]: + vals = [r[key] for r in reps] + s[key] = {"mean": float(np.mean(vals)), "std": float(np.std(vals))} + strata_keys = set() + for r in reps: + strata_keys.update(r["stratified_coverage"].keys()) + s["stratified_coverage"] = { + k: { + "mean": float(np.mean([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "std": float(np.std([r["stratified_coverage"][k] for r in reps if k in r["stratified_coverage"]])), + "n_reps": int(sum(k in r["stratified_coverage"] for r in reps)), + } + for k in sorted(strata_keys, key=int) + } + if "volume_ratio_by_strata" in reps[0]: + vol_keys = set() + for r in reps: + vol_keys.update(r["volume_ratio_by_strata"].keys()) + s["volume_ratio_by_strata"] = { + k: { + "mean": float(np.mean([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "std": float(np.std([r["volume_ratio_by_strata"][k] for r in reps if k in r["volume_ratio_by_strata"]])), + "n_reps": int(sum(k in r["volume_ratio_by_strata"] for r in reps)), + } + for k in sorted(vol_keys, key=int) + } + summary[m] = s + log.info( + f" {m:12s} cov={s['marginal_coverage']['mean']:.3f}±{s['marginal_coverage']['std']:.3f} " + f"disp={s['max_disparity']['mean']:.3f}±{s['max_disparity']['std']:.3f}" + ) + + out_dir = Path(args.output_dir) / "tables" + out_dir.mkdir(parents=True, exist_ok=True) + suffix = f"_{args.tag}" if args.tag else "" + out_file = out_dir / f"exp2_5_topics_K{K}{suffix}.json" + with open(out_file, "w") as f: + json.dump(dict(summary=summary, K=K, n=len(Y), + config=vars(args), raw=all_results), f, indent=2) + log.info(f"Saved to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/summarize_real_strata_sensitivity.py b/scripts/summarize_real_strata_sensitivity.py new file mode 100644 index 0000000000000000000000000000000000000000..e73e485cc58faf247d93451cefa35f8e1e31b1d5 --- /dev/null +++ b/scripts/summarize_real_strata_sensitivity.py @@ -0,0 +1,366 @@ +"""Summarize all-real-task stratification sensitivity runs.""" +from __future__ import annotations + +import argparse +import csv +import json +from collections import Counter +from pathlib import Path + +from make_tables import METHOD_LABELS + + +TASK_FILES = { + "cifar10": "exp2_2_softmax_cifar10_strata_{strata}_fixed.json", + "topics": "exp2_5_topics_K10_strata_{strata}_fixed.json", + "affectivetext": "exp2_6_affective_text_strata_{strata}_fixed.json", + "samson": "exp2_3_hyperspectral_samson_nmf_strata_{strata}_fixed.json", + "utkface": "exp2_4_age_ldl_K10_strata_{strata}_fixed.json", + "pbmc": "pbmc_sensitivity_exp2_1_bulk_deconv_{strata}_fixed.json", +} + +DISPLAY_NAMES = { + "cifar10": "CIFAR-10", + "topics": "Topics", + "affectivetext": "AffectiveText", + "samson": "Samson", + "utkface": "UTKFace", + "pbmc": "PBMC", +} + +STRATA_ORDER = ["boundary", "entropy", "dominant", "kmeans"] +EXPECTED_REPS = { + "cifar10": 50, + "topics": 50, + "affectivetext": 50, + "samson": 50, + "utkface": 50, + "pbmc": 200, +} + + +def extract_n_rep(data: dict) -> int | None: + config = data.get("config", {}) + for key in ("n_rep", "n_reps"): + if key in config: + return int(config[key]) + conformal = config.get("conformal", {}) + for key in ("n_rep", "n_reps"): + if key in conformal: + return int(conformal[key]) + evaluation = config.get("evaluation", {}) + for key in ("n_rep", "n_reps"): + if key in evaluation: + return int(evaluation[key]) + return None + + +def load_summary(path: Path) -> tuple[dict, float, int | None]: + data = json.loads(path.read_text()) + summary = data.get("summary", data.get("aggregated")) + config = data.get("config", {}) + + alpha = None + if "alpha" in config: + alpha = float(config["alpha"]) + elif "conformal" in config and "alpha" in config["conformal"]: + alpha = float(config["conformal"]["alpha"]) + if alpha is None: + alpha = 0.1 + + return summary, alpha, extract_n_rep(data) + + +def metric_mean(entry: dict, key: str) -> float: + value = entry.get(key, {}) + if isinstance(value, dict): + return float(value.get("mean", float("nan"))) + return float(value) + + +def rank_methods(summary: dict, alpha: float, coverage_tol: float) -> tuple[list[str], list[str]]: + nominal = 1.0 - alpha + methods = list(summary) + + valid = [ + method for method in methods + if metric_mean(summary[method], "marginal_coverage") >= nominal - coverage_tol + ] + valid_sorted = sorted( + valid, + key=lambda m: ( + metric_mean(summary[m], "max_disparity"), + metric_mean(summary[m], "mean_radius"), + -metric_mean(summary[m], "marginal_coverage"), + ), + ) + raw_sorted = sorted( + methods, + key=lambda m: ( + metric_mean(summary[m], "max_disparity"), + metric_mean(summary[m], "mean_radius"), + -metric_mean(summary[m], "marginal_coverage"), + ), + ) + return valid_sorted, raw_sorted + + +def summarize(input_dir: Path, coverage_tol: float) -> tuple[list[dict], list[dict]]: + detail_rows: list[dict] = [] + winner_rows: list[dict] = [] + + for task, template in TASK_FILES.items(): + winners = [] + for strata in STRATA_ORDER: + path = input_dir / template.format(strata=strata) + if not path.exists(): + winner_rows.append({ + "task": task, + "task_label": DISPLAY_NAMES[task], + "strata": strata, + "status": "missing", + }) + continue + + summary, alpha, n_rep = load_summary(path) + expected_reps = EXPECTED_REPS[task] + if n_rep is None or n_rep < expected_reps: + winner_rows.append({ + "task": task, + "task_label": DISPLAY_NAMES[task], + "strata": strata, + "status": "incomplete", + "observed_n_rep": n_rep, + "expected_n_rep": expected_reps, + }) + continue + valid_rank, raw_rank = rank_methods(summary, alpha, coverage_tol) + nominal = 1.0 - alpha + + for rank, method in enumerate(raw_rank, start=1): + entry = summary[method] + detail_rows.append({ + "task": task, + "task_label": DISPLAY_NAMES[task], + "strata": strata, + "method": method, + "rank_raw": rank, + "is_valid": metric_mean(entry, "marginal_coverage") >= nominal - coverage_tol, + "marginal_coverage": metric_mean(entry, "marginal_coverage"), + "max_disparity": metric_mean(entry, "max_disparity"), + "worst_stratum_coverage": metric_mean(entry, "worst_stratum_coverage"), + "mean_radius": metric_mean(entry, "mean_radius"), + "runtime_sec": metric_mean(entry, "runtime_sec"), + "n_rep": n_rep, + }) + + if valid_rank: + best = valid_rank[0] + winners.append(best) + best_entry = summary[best] + winner_rows.append({ + "task": task, + "task_label": DISPLAY_NAMES[task], + "strata": strata, + "status": "ok", + "best_valid_method": best, + "best_valid_coverage": metric_mean(best_entry, "marginal_coverage"), + "best_valid_disparity": metric_mean(best_entry, "max_disparity"), + "best_valid_radius": metric_mean(best_entry, "mean_radius"), + "best_raw_method": raw_rank[0] if raw_rank else "", + "valid_ranking": valid_rank, + "raw_ranking": raw_rank, + }) + else: + winner_rows.append({ + "task": task, + "task_label": DISPLAY_NAMES[task], + "strata": strata, + "status": "no_valid_method", + "best_raw_method": raw_rank[0] if raw_rank else "", + "valid_ranking": [], + "raw_ranking": raw_rank, + }) + + if winners: + counts = Counter(winners) + modal, modal_count = counts.most_common(1)[0] + winner_rows.append({ + "task": task, + "task_label": DISPLAY_NAMES[task], + "strata": "_summary", + "status": "task_summary", + "modal_best_valid_method": modal, + "modal_count": modal_count, + "winner_stable": modal_count == len(STRATA_ORDER), + "winner_set": sorted(counts), + }) + + return detail_rows, winner_rows + + +def write_csv(path: Path, rows: list[dict]) -> None: + if not rows: + return + fieldnames = list(rows[0].keys()) + with open(path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def write_markdown(path: Path, winner_rows: list[dict]) -> None: + per_task = {} + for row in winner_rows: + task = row["task"] + per_task.setdefault(task, {}) + per_task[task][row["strata"]] = row + + lines = [ + "# Real-Task Stratification Sensitivity Summary", + "", + "Rows report the best valid method under each fixed stratification.", + "A method is considered valid if mean marginal coverage is at least nominal minus the configured tolerance.", + "", + "| Task | Boundary | Entropy | Dominant | KMeans | Stable winner? |", + "|---|---|---|---|---|---|", + ] + + for task in TASK_FILES: + task_rows = per_task.get(task, {}) + cells = [] + for strata in STRATA_ORDER: + row = task_rows.get(strata) + if not row or row.get("status") != "ok": + if row and row.get("status") == "incomplete": + cells.append("incomplete") + else: + cells.append("missing") + continue + cells.append( + f"{row['best_valid_method']} " + f"({row['best_valid_disparity']:.3f}, cov={row['best_valid_coverage']:.3f})" + ) + summary = task_rows.get("_summary", {}) + stable = summary.get("winner_stable") + if stable is True: + stable_text = f"yes ({summary.get('modal_best_valid_method')})" + elif stable is False: + stable_text = ", ".join(summary.get("winner_set", [])) + else: + stable_text = "pending" + lines.append( + f"| {DISPLAY_NAMES[task]} | {cells[0]} | {cells[1]} | {cells[2]} | {cells[3]} | {stable_text} |" + ) + + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def latex_escape(text: str) -> str: + return ( + text.replace("\\", "\\textbackslash{}") + .replace("_", "\\_") + .replace("%", "\\%") + .replace("&", "\\&") + .replace("#", "\\#") + ) + + +def format_cell(row: dict | None) -> str: + if not row or row.get("status") != "ok": + if row and row.get("status") == "incomplete": + return "incomplete" + return "--" + method = METHOD_LABELS.get(row["best_valid_method"], row["best_valid_method"]) + disparity = row["best_valid_disparity"] + coverage = row["best_valid_coverage"] + return f"{latex_escape(method)} ({disparity:.3f}, {coverage:.3f})" + + +def write_latex(path: Path, winner_rows: list[dict]) -> None: + per_task = {} + for row in winner_rows: + task = row["task"] + per_task.setdefault(task, {}) + per_task[task][row["strata"]] = row + + lines = [ + "% Auto-generated by scripts/summarize_real_strata_sensitivity.py", + "\\begin{table*}[t]", + "\\centering", + "\\caption{Real-task stratification sensitivity across fixed alternative strata. Each cell reports the best valid method under that stratification, shown as method name with $(\\text{max disparity}, \\text{marginal coverage})$. A method is treated as valid when mean marginal coverage is at least nominal minus the configured tolerance.}", + "\\label{tab:real-strata-sensitivity}", + "\\scriptsize", + "\\resizebox{\\textwidth}{!}{%", + "\\begin{tabular}{@{}lccccp{3.3cm}@{}}", + "\\toprule", + "Task & Boundary & Entropy & Dominant & KMeans & Winner stability \\\\", + "\\midrule", + ] + + for task in TASK_FILES: + task_rows = per_task.get(task, {}) + summary = task_rows.get("_summary", {}) + stable = summary.get("winner_stable") + if stable is True: + stability_text = f"Stable: {METHOD_LABELS.get(summary.get('modal_best_valid_method', ''), summary.get('modal_best_valid_method', ''))}" + elif stable is False: + winners = [ + METHOD_LABELS.get(name, name) + for name in summary.get("winner_set", []) + ] + stability_text = "Mixed: " + ", ".join(winners) + else: + stability_text = "Pending" + + row = [ + latex_escape(DISPLAY_NAMES[task]), + format_cell(task_rows.get("boundary")), + format_cell(task_rows.get("entropy")), + format_cell(task_rows.get("dominant")), + format_cell(task_rows.get("kmeans")), + latex_escape(stability_text), + ] + lines.append(" & ".join(row) + " \\\\") + + lines.extend([ + "\\bottomrule", + "\\end{tabular}", + "}", + "\\end{table*}", + ]) + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--input-dir", default="results/tables") + parser.add_argument("--coverage-tol", type=float, default=0.02) + parser.add_argument( + "--detail-csv", + default="results/tables/real_strata_sensitivity_detail.csv", + ) + parser.add_argument( + "--winner-json", + default="results/tables/real_strata_sensitivity_winners.json", + ) + parser.add_argument( + "--winner-md", + default="results/tables/real_strata_sensitivity_summary.md", + ) + parser.add_argument( + "--winner-tex", + default="paper/rewrite_2026/latex/generated_real_strata_sensitivity.tex", + ) + args = parser.parse_args() + + detail_rows, winner_rows = summarize(Path(args.input_dir), args.coverage_tol) + + write_csv(Path(args.detail_csv), detail_rows) + Path(args.winner_json).write_text(json.dumps(winner_rows, indent=2), encoding="utf-8") + write_markdown(Path(args.winner_md), winner_rows) + write_latex(Path(args.winner_tex), winner_rows) + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_simplextasks_12.py b/scripts/validate_simplextasks_12.py new file mode 100644 index 0000000000000000000000000000000000000000..9dd9cef9cea00d6a41a5e431aac368c4ab6ddf79 --- /dev/null +++ b/scripts/validate_simplextasks_12.py @@ -0,0 +1,173 @@ +"""Validate the SimplexTasks-12 release package.""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + +import numpy as np + +REQUIRED_MANIFEST_KEYS = { + "name", + "slug", + "version", + "seed", + "task_count", + "real_tasks", + "synthetic_tasks", +} + + +def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + while True: + chunk = f.read(chunk_size) + if not chunk: + break + h.update(chunk) + return h.hexdigest() + + +def check_simplex(name: str, arr: np.ndarray, tol: float = 1e-4) -> list[str]: + issues = [] + if arr.ndim != 2: + issues.append(f"{name} must be 2D, got shape {arr.shape}") + return issues + if np.any(arr < -tol): + issues.append(f"{name} contains values below 0") + row_sums = arr.sum(axis=1) + max_dev = float(np.max(np.abs(row_sums - 1.0))) + if max_dev > tol: + issues.append(f"{name} rows do not sum to 1 within tolerance; max deviation={max_dev:.2e}") + return issues + + +def validate_task(task_dir: Path, meta: dict) -> dict: + npz_path = task_dir / "task.npz" + meta_path = task_dir / "metadata.json" + issues: list[str] = [] + warnings: list[str] = [] + + if not npz_path.exists(): + issues.append("Missing task.npz") + return {"task_id": meta.get("task_id", task_dir.name), "issues": issues, "warnings": warnings} + if not meta_path.exists(): + issues.append("Missing metadata.json") + return {"task_id": meta.get("task_id", task_dir.name), "issues": issues, "warnings": warnings} + + data = np.load(npz_path, allow_pickle=False) + files = sorted(data.files) + declared = sorted(meta.get("available_arrays", [])) + if files != declared: + issues.append(f"available_arrays mismatch: metadata={declared}, npz={files}") + + if "Y" not in data.files or "U" not in data.files: + issues.append("Both Y and U must be present") + else: + Y = data["Y"] + U = data["U"] + if Y.shape != U.shape: + issues.append(f"Y and U shape mismatch: {Y.shape} vs {U.shape}") + else: + if meta.get("n_samples") != int(Y.shape[0]): + issues.append(f"n_samples mismatch: metadata={meta.get('n_samples')} actual={Y.shape[0]}") + if meta.get("simplex_dim") != int(Y.shape[1]): + issues.append(f"simplex_dim mismatch: metadata={meta.get('simplex_dim')} actual={Y.shape[1]}") + issues.extend(check_simplex("Y", Y)) + issues.extend(check_simplex("U", U)) + + if meta.get("subset") == "synthetic": + for required in ["X", "R", "sigma_true", "split"]: + if required not in data.files: + issues.append(f"Synthetic task missing {required}") + if "split" in data.files: + split = data["split"] + if split.shape[0] != meta.get("n_samples"): + issues.append("split length does not match n_samples") + unique = sorted({str(x) for x in split.tolist()}) + if not set(unique).issubset({"train", "scale", "cal", "test"}): + issues.append(f"Unexpected split labels: {unique}") + if not (task_dir / "config.yaml").exists(): + issues.append("Synthetic task missing config.yaml") + else: + redistribution = meta.get("redistribution") + if redistribution not in {"derived-only", "source-cited", "open"}: + issues.append(f"Unexpected redistribution value: {redistribution}") + if "notes" not in meta: + warnings.append("metadata.notes missing") + + task_sha = sha256_file(npz_path) + return { + "task_id": meta["task_id"], + "subset": meta["subset"], + "task_npz_sha256": task_sha, + "task_npz_bytes": npz_path.stat().st_size, + "issues": issues, + "warnings": warnings, + } + + +def validate_package(root: Path) -> dict: + manifest_path = root / "manifest.json" + if not manifest_path.exists(): + raise FileNotFoundError(f"Missing manifest: {manifest_path}") + + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + missing_manifest = sorted(REQUIRED_MANIFEST_KEYS - set(manifest)) + if missing_manifest: + raise ValueError(f"Manifest missing keys: {missing_manifest}") + + task_entries = manifest["real_tasks"] + manifest["synthetic_tasks"] + if len(task_entries) != manifest["task_count"]: + raise ValueError( + f"task_count mismatch: manifest says {manifest['task_count']} but lists {len(task_entries)} tasks" + ) + + report = { + "name": manifest["name"], + "version": manifest["version"], + "task_count": manifest["task_count"], + "task_reports": [], + "issues": [], + "warnings": [], + } + + for task_meta in task_entries: + task_dir = root / task_meta["subset"] / task_meta["task_id"] + if not task_dir.exists(): + report["issues"].append(f"Missing task directory: {task_dir.relative_to(root)}") + continue + task_report = validate_task(task_dir, task_meta) + report["task_reports"].append(task_report) + report["issues"].extend([f"{task_meta['task_id']}: {x}" for x in task_report["issues"]]) + report["warnings"].extend([f"{task_meta['task_id']}: {x}" for x in task_report["warnings"]]) + + for required in ["README.md", "LICENSE_NOTES.md"]: + if not (root / required).exists(): + report["issues"].append(f"Missing root file: {required}") + + return report + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--root", default="release/simplextasks-12") + parser.add_argument("--write-report", action="store_true") + args = parser.parse_args() + + root = Path(args.root) + report = validate_package(root) + print(json.dumps(report, indent=2)) + + if args.write_report: + out_path = root / "VALIDATION_REPORT.json" + out_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + + if report["issues"]: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/data/__init__.py b/src/data/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1d32b0a73faa76af922fc72b387f9a85ec69b46e --- /dev/null +++ b/src/data/__init__.py @@ -0,0 +1,7 @@ +from .affective_text import ( + EMOTION_NAMES, + build_prediction_matrix, + load_affective_text, + load_prediction_cache, +) + diff --git a/src/data/affective_text.py b/src/data/affective_text.py new file mode 100644 index 0000000000000000000000000000000000000000..8a82716dc3e129ba8830567e21872a9f9c0ae8af --- /dev/null +++ b/src/data/affective_text.py @@ -0,0 +1,158 @@ +"""Utilities for SemEval-2007 Affective Text emotion-composition benchmark.""" +from __future__ import annotations + +import json +import re +import xml.etree.ElementTree as ET +from pathlib import Path + +import numpy as np + +EMOTION_NAMES = ["anger", "disgust", "fear", "joy", "sadness", "surprise"] + + +def _candidate_xml_files(root: Path) -> list[Path]: + candidates = [] + for pattern in [ + "**/*test*.xml", + "**/*headline*.xml", + "**/*.xml", + ]: + candidates.extend(sorted(root.glob(pattern))) + seen = set() + out = [] + for path in candidates: + if path in seen: + continue + seen.add(path) + out.append(path) + return out + + +def _candidate_gold_files(root: Path) -> list[Path]: + candidates = [] + for pattern in [ + "**/*test*emotion*.gold*", + "**/*emotion*.gold*", + "**/*test*gold*", + "**/*emotion*txt", + "**/*gold*", + ]: + candidates.extend(sorted(root.glob(pattern))) + seen = set() + out = [] + for path in candidates: + if path in seen: + continue + seen.add(path) + out.append(path) + return out + + +def _parse_headline_xml(path: Path) -> dict[str, str]: + text = path.read_text(encoding="utf-8", errors="replace") + matches = re.findall(r'(.*?)', text, flags=re.DOTALL) + if matches: + return {str(idx): body.strip() for idx, body in matches if body.strip()} + + tree = ET.parse(path) + root = tree.getroot() + headlines = {} + for elem in root.iter(): + elem_id = elem.attrib.get("id") + body = (elem.text or "").strip() + if elem_id and body: + headlines[str(elem_id)] = body + return headlines + + +def _parse_gold_file(path: Path) -> dict[str, np.ndarray]: + gold = {} + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + parts = re.split(r"[\s,;]+", line) + if len(parts) < 7: + continue + idx = parts[0] + values = [float(x) for x in parts[1:7]] + gold[str(idx)] = np.asarray(values, dtype=float) + return gold + + +def _normalize_rows(x: np.ndarray) -> np.ndarray: + x = np.maximum(np.asarray(x, dtype=float), 1e-8) + return x / x.sum(axis=1, keepdims=True) + + +def load_affective_text(data_dir: str | Path) -> dict[str, object]: + """Load SemEval-2007 Affective Text headlines and gold emotion scores. + + Expects the downloaded archive to be extracted under `data_dir`. + The loader is intentionally permissive about internal filenames. + """ + root = Path(data_dir) + if not root.exists(): + raise FileNotFoundError(f"Data directory not found: {root}") + + headline_map = {} + best_n = -1 + for path in _candidate_xml_files(root): + parsed = _parse_headline_xml(path) + if len(parsed) > best_n: + headline_map = parsed + best_n = len(parsed) + if not headline_map: + raise FileNotFoundError(f"Could not find headline XML under {root}") + + gold_map = {} + best_n = -1 + for path in _candidate_gold_files(root): + parsed = _parse_gold_file(path) + if len(parsed) > best_n: + gold_map = parsed + best_n = len(parsed) + if not gold_map: + raise FileNotFoundError(f"Could not find gold emotion scores under {root}") + + common_ids = sorted(set(headline_map) & set(gold_map), key=lambda x: int(re.sub(r"\D", "", x) or x)) + if not common_ids: + raise ValueError("No shared instance ids between headlines and gold scores") + + headlines = [headline_map[i] for i in common_ids] + raw_scores = np.vstack([gold_map[i] for i in common_ids]) + Y = _normalize_rows(raw_scores) + return { + "ids": common_ids, + "headlines": headlines, + "raw_scores": raw_scores, + "Y": Y, + "emotions": EMOTION_NAMES, + } + + +def load_prediction_cache(cache_path: str | Path) -> dict[str, dict]: + cache = {} + with open(cache_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + row = json.loads(line) + cache[str(row["id"])] = row + return cache + + +def build_prediction_matrix( + ids: list[str], + cache_path: str | Path, +) -> tuple[np.ndarray, np.ndarray]: + cache = load_prediction_cache(cache_path) + missing = [idx for idx in ids if idx not in cache] + if missing: + raise ValueError(f"Missing predictions for {len(missing)} ids in {cache_path}") + + raw_scores = np.vstack([np.asarray(cache[idx]["scores"], dtype=float) for idx in ids]) + U = _normalize_rows(raw_scores) + return raw_scores, U diff --git a/src/dgp/__init__.py b/src/dgp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a0e319ce51cd6e8add2130a5aa8fdb0fa1ed92 --- /dev/null +++ b/src/dgp/__init__.py @@ -0,0 +1,12 @@ +"""Synthetic DGP registry exports. + +Keep this module lightweight: real-data helpers such as pseudo-bulk generation +and NNLS deconvolution should be imported from their submodules directly so +synthetic experiments do not pay the import cost of heavy optional stacks. +""" + +from .discrete_groups import DiscreteGroupsDGP +from .heavy_tail import HeavyTailDGP +from .high_k import HighKDGP +from .model_bias import ModelBiasDGP +from .pure_scale import PureScaleDGP diff --git a/src/dgp/base.py b/src/dgp/base.py new file mode 100644 index 0000000000000000000000000000000000000000..aeae34fb9dd4fc4ecdc74a847dc4f028f087297f --- /dev/null +++ b/src/dgp/base.py @@ -0,0 +1,27 @@ +"""Base DGP interface.""" +from abc import ABC, abstractmethod +from dataclasses import dataclass +import numpy as np + + +@dataclass +class DGPSample: + """Standard output of any DGP. + + X: covariates (n, d) + Y: true simplex responses (n, K) + U: predictions f̂(X) (n, K) + R: residuals d(Y, U) (n,) + sigma_true: oracle local scale at U (n,), None if unavailable + """ + X: np.ndarray + Y: np.ndarray + U: np.ndarray + R: np.ndarray + sigma_true: np.ndarray | None = None + + +class BaseDGP(ABC): + @abstractmethod + def sample(self, n: int, rng: np.random.Generator) -> DGPSample: + ... diff --git a/src/dgp/deconv.py b/src/dgp/deconv.py new file mode 100644 index 0000000000000000000000000000000000000000..c45598008a572aeedf9da4406f0dec961a08dc87 --- /dev/null +++ b/src/dgp/deconv.py @@ -0,0 +1,34 @@ +"""Bulk deconvolution via constrained least squares.""" +import numpy as np +from scipy.optimize import nnls +import logging + +log = logging.getLogger(__name__) + + +def nnls_deconv(bulk: np.ndarray, signature: np.ndarray) -> np.ndarray: + """Deconvolve bulk expression into cell type proportions via NNLS. + + Solves: bulk_i ≈ signature @ p_i, s.t. p_i >= 0, then normalizes to simplex. + + Args: + bulk: (n_samples, n_genes) + signature: (n_genes, K) + + Returns: + proportions_hat: (n_samples, K), rows sum to 1 + """ + n = bulk.shape[0] + K = signature.shape[1] + props = np.zeros((n, K)) + + for i in range(n): + coef, _ = nnls(signature, bulk[i]) + total = coef.sum() + if total > 0: + props[i] = coef / total + else: + props[i] = 1.0 / K # fallback to uniform + + log.info(f"Deconvolved {n} samples into {K} types") + return props diff --git a/src/dgp/discrete_groups.py b/src/dgp/discrete_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..9f93a4391dfef862ec18826b93397ee3aa286962 --- /dev/null +++ b/src/dgp/discrete_groups.py @@ -0,0 +1,41 @@ +"""DGP with discrete heterogeneity aligned to coarse simplex groups.""" +import numpy as np + +from .base import BaseDGP, DGPSample +from .pure_scale import PureScaleDGP +from ..utils.simplex import aitchison_dist, ilr, ilr_inv + + +class DiscreteGroupsDGP(PureScaleDGP): + """Step-function scale heterogeneity based on the predicted top class.""" + + def __init__( + self, + K: int = 10, + sigma_low: float = 0.08, + sigma_high: float = 0.30, + d_x: int = 5, + easy_classes: int = 5, + ): + super().__init__(K=K, sigma_min=sigma_low, c=sigma_high - sigma_low, d_x=d_x) + self.sigma_low = sigma_low + self.sigma_high = sigma_high + self.easy_classes = easy_classes + + def _sigma(self, u: np.ndarray) -> np.ndarray: + top_class = np.argmax(u, axis=1) + is_easy = top_class < self.easy_classes + return np.where(is_easy, self.sigma_low, self.sigma_high) + + def sample(self, n: int, rng: np.random.Generator) -> DGPSample: + self._init_weights(rng) + X = rng.standard_normal((n, self.d_x)) + mu = self._mu(X) + sigma = self._sigma(mu) + + Z_mu = ilr(mu) + eps = rng.standard_normal((n, self.K - 1)) + Y = ilr_inv(Z_mu + sigma[:, None] * eps, K=self.K) + U = mu + R = aitchison_dist(Y, U) + return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma) diff --git a/src/dgp/heavy_tail.py b/src/dgp/heavy_tail.py new file mode 100644 index 0000000000000000000000000000000000000000..440cc0a8d67b69356fd214eb8be759b5365d27d7 --- /dev/null +++ b/src/dgp/heavy_tail.py @@ -0,0 +1,36 @@ +"""Pure-scale heterogeneity with heavy-tailed ILR noise.""" +import numpy as np + +from .base import DGPSample +from .pure_scale import PureScaleDGP +from ..utils.simplex import aitchison_dist, ilr, ilr_inv + + +class HeavyTailDGP(PureScaleDGP): + """D2 with Student-t noise in ILR space.""" + + def __init__( + self, + K: int = 3, + sigma_min: float = 0.1, + c: float = 0.5, + d_x: int = 2, + df: float = 3.0, + ): + super().__init__(K=K, sigma_min=sigma_min, c=c, d_x=d_x) + self.df = df + + def sample(self, n: int, rng: np.random.Generator) -> DGPSample: + self._init_weights(rng) + X = rng.standard_normal((n, self.d_x)) + mu = self._mu(X) + sigma = self._sigma(mu) + + Z_mu = ilr(mu) + scale = np.sqrt(self.df / (self.df - 2.0)) if self.df > 2 else 1.0 + eps = rng.standard_t(df=self.df, size=(n, self.K - 1)) / scale + Y = ilr_inv(Z_mu + sigma[:, None] * eps, K=self.K) + + U = mu + R = aitchison_dist(Y, U) + return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma) diff --git a/src/dgp/high_k.py b/src/dgp/high_k.py new file mode 100644 index 0000000000000000000000000000000000000000..e095d6812395c81c9364c43e6636befed1223090 --- /dev/null +++ b/src/dgp/high_k.py @@ -0,0 +1,15 @@ +"""High-dimensional simplex DGP.""" +from .pure_scale import PureScaleDGP + + +class HighKDGP(PureScaleDGP): + """Pure-scale heterogeneity in a high-K simplex regime.""" + + def __init__( + self, + K: int = 50, + sigma_min: float = 0.05, + c: float = 0.35, + d_x: int = 10, + ): + super().__init__(K=K, sigma_min=sigma_min, c=c, d_x=d_x) diff --git a/src/dgp/model_bias.py b/src/dgp/model_bias.py new file mode 100644 index 0000000000000000000000000000000000000000..2fbbe71b22864237c0d212ea139669ec4bdeb712 --- /dev/null +++ b/src/dgp/model_bias.py @@ -0,0 +1,72 @@ +"""Model-bias DGP: predictor misspecification beyond pure local scale.""" +import numpy as np +from .base import BaseDGP, DGPSample +from .pure_scale import PureScaleDGP +from ..utils.simplex import ilr, ilr_inv, entropy, aitchison_dist + + +class ModelBiasDGP(PureScaleDGP): + """Predictor bias that cannot be summarized by a scalar local scale alone. + + The data-generating truth still follows the pure-scale construction, but the + predictor is shifted in ILR space by a location-dependent bias field. + Depending on `bias_type`, this field can rotate with prediction-space angle + or vary smoothly with the covariates. This creates residual distributions + whose heterogeneity is not purely radial. + """ + def __init__(self, K: int = 3, sigma_min: float = 0.1, c: float = 0.5, + d_x: int = 2, bias_scale: float = 0.15, bias_type: str = "smooth"): + super().__init__(K=K, sigma_min=sigma_min, c=c, d_x=d_x) + self.bias_scale = bias_scale + self.bias_type = bias_type + self._B = None # linear bias weights + + def _init_bias(self, rng: np.random.Generator): + if self._B is None: + self._B = rng.standard_normal((self.d_x, self.K - 1)) * 0.5 + + def _bias(self, X: np.ndarray, Z_mu: np.ndarray) -> np.ndarray: + """Location-dependent predictor bias in ILR space.""" + if self.bias_type == "linear": + return self.bias_scale * (X @ self._B) + + if self.bias_type == "smooth": + return self.bias_scale * np.sin(X @ self._B) + + if self.bias_type == "rotational": + bias = np.zeros_like(Z_mu) + z0 = Z_mu[:, 0] + z1 = Z_mu[:, 1] if Z_mu.shape[1] > 1 else np.zeros_like(z0) + phase = np.arctan2(z1, z0 + 1e-8) + radius = np.sqrt(z0 ** 2 + z1 ** 2) + amp = self.bias_scale * (0.5 + np.tanh(radius)) + bias[:, 0] = amp * np.cos(2.0 * phase) + if Z_mu.shape[1] > 1: + bias[:, 1] = amp * np.sin(2.0 * phase) + if Z_mu.shape[1] > 2: + bias[:, 2:] = 0.35 * self.bias_scale * np.sin(1.7 * Z_mu[:, 2:]) + return bias + + raise ValueError(f"Unknown bias_type: {self.bias_type}") + + def sample(self, n: int, rng: np.random.Generator) -> DGPSample: + self._init_weights(rng) + self._init_bias(rng) + + X = rng.standard_normal((n, self.d_x)) + mu = self._mu(X) + sigma = self._sigma(mu) + + # True Y: same as DGP 1 + Z_mu = ilr(mu) + eps = rng.standard_normal((n, self.K - 1)) + Z_y = Z_mu + sigma[:, None] * eps + Y = ilr_inv(Z_y, K=self.K) + + # Biased predictor + b = self._bias(X, Z_mu) + U = ilr_inv(Z_mu + b, K=self.K) + + R = aitchison_dist(Y, U) + + return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma) diff --git a/src/dgp/pseudobulk.py b/src/dgp/pseudobulk.py new file mode 100644 index 0000000000000000000000000000000000000000..f9bc410f58869fe15aff03d056cdc20c700e7bae --- /dev/null +++ b/src/dgp/pseudobulk.py @@ -0,0 +1,191 @@ +"""Semi-synthetic pseudo-bulk generation from scRNA-seq reference.""" +import numpy as np +import logging +from dataclasses import dataclass + +log = logging.getLogger(__name__) + + +@dataclass +class PseudoBulkDataset: + """Output of pseudo-bulk generation. + + bulk: pseudo-bulk expression matrix (n_samples, n_genes) + proportions: true cell type proportions (n_samples, K) + signature: cell-type signature matrix (n_genes, K) + cell_type_names: list of cell type names + gene_names: list of gene names + """ + bulk: np.ndarray + proportions: np.ndarray + signature: np.ndarray + cell_type_names: list[str] + gene_names: list[str] + + +def load_scrna_reference(h5ad_path: str, celltype_key: str = "cell_type", + min_cells_per_type: int = 50, + n_top_genes: int = 2000): + """Load scRNA-seq reference from h5ad, return expression matrix and labels. + + Args: + h5ad_path: path to .h5ad file + celltype_key: obs column with cell type labels + min_cells_per_type: drop types with fewer cells + n_top_genes: number of highly variable genes to keep + + Returns: + expr: expression matrix (n_cells, n_genes), dense, counts or normalized + labels: cell type labels (n_cells,) + gene_names: list of gene names + cell_type_names: list of retained cell types + """ + import scanpy as sc + + adata = sc.read_h5ad(h5ad_path) + log.info(f"Loaded {adata.n_obs} cells, {adata.n_vars} genes") + + # Filter cell types with too few cells + type_counts = adata.obs[celltype_key].value_counts() + keep_types = type_counts[type_counts >= min_cells_per_type].index.tolist() + adata = adata[adata.obs[celltype_key].isin(keep_types)].copy() + log.info(f"Kept {len(keep_types)} cell types, {adata.n_obs} cells") + + # Normalize + select HVGs + sc.pp.normalize_total(adata, target_sum=1e4) + sc.pp.log1p(adata) + sc.pp.highly_variable_genes(adata, n_top_genes=n_top_genes) + adata = adata[:, adata.var["highly_variable"]].copy() + + # Dense matrix + expr = adata.X + if hasattr(expr, "toarray"): + expr = expr.toarray() + expr = np.asarray(expr, dtype=np.float64) + + labels = adata.obs[celltype_key].values + gene_names = adata.var_names.tolist() + cell_type_names = sorted(keep_types) + + return expr, labels, gene_names, cell_type_names + + +def build_signature(expr: np.ndarray, labels: np.ndarray, + cell_type_names: list[str]) -> np.ndarray: + """Build signature matrix: mean expression per cell type. + + Args: + expr: (n_cells, n_genes) + labels: (n_cells,) + cell_type_names: ordered cell type names + + Returns: + signature: (n_genes, K) + """ + n_genes = expr.shape[1] + K = len(cell_type_names) + sig = np.zeros((n_genes, K)) + for k, ct in enumerate(cell_type_names): + mask = labels == ct + sig[:, k] = expr[mask].mean(axis=0) + return sig + + +def sample_proportions(n: int, K: int, rng: np.random.Generator, + concentration: float = 1.0) -> np.ndarray: + """Sample random proportions from Dirichlet. + + Args: + n: number of samples + K: number of cell types + rng: numpy random generator + concentration: Dirichlet concentration. + <1: sparse/peaky, =1: uniform, >1: balanced + + Returns: + proportions: (n, K), rows sum to 1 + """ + alpha = np.full(K, concentration) + return rng.dirichlet(alpha, size=n) + + +def generate_pseudobulk( + expr: np.ndarray, + labels: np.ndarray, + cell_type_names: list[str], + gene_names: list[str], + n_samples: int = 5000, + cells_per_sample: int = 200, + concentration: float = 1.0, + noise_sd: float = 0.1, + seed: int = 2026, +) -> PseudoBulkDataset: + """Generate pseudo-bulk dataset with known ground-truth proportions. + + For each sample: + 1. Draw proportions from Dirichlet(concentration) + 2. Sample cells_per_sample cells according to proportions + 3. Sum their expression to get pseudo-bulk + 4. Add optional Gaussian noise + + Args: + expr: scRNA-seq expression (n_cells, n_genes) + labels: cell type labels (n_cells,) + cell_type_names: ordered cell type names + gene_names: gene names + n_samples: number of pseudo-bulk samples + cells_per_sample: cells mixed per sample + concentration: Dirichlet parameter + noise_sd: Gaussian noise on log-expression (0 = no noise) + seed: random seed + + Returns: + PseudoBulkDataset + """ + rng = np.random.default_rng(seed) + K = len(cell_type_names) + n_genes = expr.shape[1] + + # Index cells by type + type_indices = {} + for k, ct in enumerate(cell_type_names): + type_indices[ct] = np.where(labels == ct)[0] + + # Sample proportions + props = sample_proportions(n_samples, K, rng, concentration) + + # Generate pseudo-bulk + bulk = np.zeros((n_samples, n_genes)) + actual_props = np.zeros((n_samples, K)) + + for i in range(n_samples): + # Number of cells per type (multinomial) + counts = rng.multinomial(cells_per_sample, props[i]) + actual_props[i] = counts / counts.sum() + + # Sample and sum cells + sample_expr = np.zeros(n_genes) + for k, ct in enumerate(cell_type_names): + if counts[k] > 0: + idx = rng.choice(type_indices[ct], size=counts[k], replace=True) + sample_expr += expr[idx].sum(axis=0) + + bulk[i] = sample_expr / cells_per_sample + + # Optional noise + if noise_sd > 0: + bulk = bulk + rng.normal(0, noise_sd, bulk.shape) + bulk = np.maximum(bulk, 0) + + # Build signature + sig = build_signature(expr, labels, cell_type_names) + + log.info(f"Generated {n_samples} pseudo-bulk samples, {K} types, {n_genes} genes") + + return PseudoBulkDataset( + bulk=bulk, + proportions=actual_props, + signature=sig, + cell_type_names=cell_type_names, + gene_names=gene_names, + ) diff --git a/src/dgp/pure_scale.py b/src/dgp/pure_scale.py new file mode 100644 index 0000000000000000000000000000000000000000..8abb164388b831425dabc02aa93d76a4708adb60 --- /dev/null +++ b/src/dgp/pure_scale.py @@ -0,0 +1,68 @@ +"""DGP 1: Pure scale heterogeneity in ILR space. + +Y = ilr⁻¹(ilr(μ(X)) + σ(μ(X))·ε), ε ~ N(0, I) +σ(u) = σ_min + c·(1 - H(u)/log K) +f̂(X) = μ(X) +""" +import numpy as np +from .base import BaseDGP, DGPSample +from ..utils.simplex import ilr, ilr_inv, entropy, aitchison_dist + + +def _softmax(z: np.ndarray) -> np.ndarray: + z = z - z.max(axis=-1, keepdims=True) + e = np.exp(z) + return e / e.sum(axis=-1, keepdims=True) + + +class PureScaleDGP(BaseDGP): + """Pure scale heterogeneity: residual distribution is N(0, σ²(u)I) in ILR space. + + Args: + K: number of simplex components + sigma_min: minimum scale + c: scale range (σ_max = σ_min + c) + d_x: dimension of covariate X + """ + def __init__(self, K: int = 3, sigma_min: float = 0.1, c: float = 0.5, + d_x: int = 2): + self.K = K + self.sigma_min = sigma_min + self.c = c + self.d_x = d_x + # Fixed linear map X -> logits, generated once + self._W = None + + def _init_weights(self, rng: np.random.Generator): + """Initialize fixed linear map for μ(X).""" + if self._W is None: + self._W = rng.standard_normal((self.d_x, self.K)) * 0.8 + + def _mu(self, X: np.ndarray) -> np.ndarray: + """Map X -> simplex via softmax of linear functions.""" + logits = X @ self._W + return _softmax(logits) + + def _sigma(self, u: np.ndarray) -> np.ndarray: + """Scale function: low entropy -> high scale.""" + H = entropy(u) + return self.sigma_min + self.c * (1.0 - H / np.log(self.K)) + + def sample(self, n: int, rng: np.random.Generator) -> DGPSample: + self._init_weights(rng) + + X = rng.standard_normal((n, self.d_x)) + mu = self._mu(X) + sigma = self._sigma(mu) + + # Generate Y in ILR space + Z_mu = ilr(mu) + eps = rng.standard_normal((n, self.K - 1)) + Z_y = Z_mu + sigma[:, None] * eps + Y = ilr_inv(Z_y, K=self.K) + + # Predictor = oracle mean + U = mu + R = aitchison_dist(Y, U) + + return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma) diff --git a/src/dgp/tail_drift.py b/src/dgp/tail_drift.py new file mode 100644 index 0000000000000000000000000000000000000000..f6bcf153520e482ee5a961a2f7cf3a1a2b75f0f3 --- /dev/null +++ b/src/dgp/tail_drift.py @@ -0,0 +1,46 @@ +"""Scale heterogeneity with mild tail/covariance drift.""" +import numpy as np + +from .base import DGPSample +from .pure_scale import PureScaleDGP +from ..utils.simplex import aitchison_dist, entropy, ilr, ilr_inv + + +class TailDriftDGP(PureScaleDGP): + """Pure-scale DGP with entropy-dependent anisotropic ILR noise. + + This auxiliary regime is not part of the main table, but it is useful for + stress-testing local normalization when the residual shape changes as well + as the scalar residual scale. + """ + + def __init__( + self, + K: int = 3, + sigma_min: float = 0.1, + c: float = 0.5, + d_x: int = 2, + drift_strength: float = 0.5, + ): + super().__init__(K=K, sigma_min=sigma_min, c=c, d_x=d_x) + self.drift_strength = drift_strength + + def sample(self, n: int, rng: np.random.Generator) -> DGPSample: + self._init_weights(rng) + X = rng.standard_normal((n, self.d_x)) + mu = self._mu(X) + sigma = self._sigma(mu) + + Z_mu = ilr(mu) + H = entropy(mu) / np.log(self.K) + eps = rng.standard_normal((n, self.K - 1)) + if self.K > 2: + # Low-entropy predictions get a mild directional covariance tilt. + eps[:, 0] *= 1.0 + self.drift_strength * (1.0 - H) + eps[:, 1:] *= 1.0 - 0.5 * self.drift_strength * (1.0 - H[:, None]) + else: + eps[:, 0] *= 1.0 + self.drift_strength * (1.0 - H) + Y = ilr_inv(Z_mu + sigma[:, None] * eps, K=self.K) + U = mu + R = aitchison_dist(Y, U) + return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma) diff --git a/src/methods/__init__.py b/src/methods/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..42b2bba6f35a272d28fd7cdfd9c1d90af897c59f --- /dev/null +++ b/src/methods/__init__.py @@ -0,0 +1,9 @@ +from .global_cp import global_split_conformal +from .partition_cp import partition_conformal +from .twostage import twostage_conformal +from .fullcp import full_conformal +from .oracle import oracle_conformal +from .oneshot import oneshot_conformal +from .trainres import trainres_conformal +from .jackknife_plus import jackknife_plus_conformal +from .weighted_cp import weighted_conformal diff --git a/src/methods/_knn_sigma.py b/src/methods/_knn_sigma.py new file mode 100644 index 0000000000000000000000000000000000000000..ee5ef8b6fa20eab3a1ec89ae78bc9de4024b94e6 --- /dev/null +++ b/src/methods/_knn_sigma.py @@ -0,0 +1,62 @@ +"""kNN-based local scale estimation shared by local conformal methods.""" +import numpy as np +from sklearn.neighbors import NearestNeighbors +from ..utils.simplex import ilr + + +def knn_sigma_hat( + U_ref: np.ndarray, + R_ref: np.ndarray, + U_query: np.ndarray, + k: int = 20, +) -> np.ndarray: + """Estimate local scale at query points via kNN in ILR space. + + Args: + U_ref: reference simplex predictions (n_ref, K) + R_ref: residuals at reference points (n_ref,) + U_query: query simplex predictions (n_query, K) + k: number of neighbors + + Returns: + Estimated local scale (n_query,), floored at 1e-8. + """ + if len(U_ref) == 0: + return np.ones(len(U_query), dtype=float) + Z_ref = ilr(U_ref) + Z_query = ilr(U_query) + k_actual = min(k, len(Z_ref)) + nn = NearestNeighbors(n_neighbors=k_actual).fit(Z_ref) + _, indices = nn.kneighbors(Z_query) + neighbor_R = R_ref[indices] # (n_query, k_actual) + sigma_hat = np.median(neighbor_R, axis=1) + return np.maximum(sigma_hat, 1e-8) + + +def knn_sigma_leave_one_out( + U_ref: np.ndarray, + R_ref: np.ndarray, + k: int = 20, +) -> np.ndarray: + """Estimate local scale at each reference point excluding itself. + + Args: + U_ref: reference simplex predictions (n_ref, K) + R_ref: residuals at reference points (n_ref,) + k: number of non-self neighbors + + Returns: + Leave-one-out local scales (n_ref,), floored at 1e-8. + """ + if len(U_ref) <= 1: + return np.ones(len(U_ref), dtype=float) + + Z_ref = ilr(U_ref) + k_actual = min(k + 1, len(Z_ref)) + nn = NearestNeighbors(n_neighbors=k_actual).fit(Z_ref) + _, indices = nn.kneighbors(Z_ref) + + loo_neighbors = indices[:, 1:] if k_actual > 1 else indices + neighbor_R = R_ref[loo_neighbors] + sigma_hat = np.median(neighbor_R, axis=1) + return np.maximum(sigma_hat, 1e-8) diff --git a/src/methods/_split_quantile.py b/src/methods/_split_quantile.py new file mode 100644 index 0000000000000000000000000000000000000000..3360a854b00d23a0f1183e4378198d3c2a0f8b3f --- /dev/null +++ b/src/methods/_split_quantile.py @@ -0,0 +1,22 @@ +"""Shared split-conformal quantile utilities.""" +import numpy as np + + +def split_conformal_quantile(values: np.ndarray, alpha: float) -> float: + """Return the standard split-conformal threshold. + + The conformal order statistic is + ``ceil((1 - alpha) * (n + 1))`` among ``n`` calibration scores. If that + order statistic is beyond the calibration sample, the finite-sample + conformal set is conservatively infinite. + """ + values = np.asarray(values, dtype=float) + n = len(values) + if n == 0: + return np.inf + if not 0.0 < alpha < 1.0: + raise ValueError("alpha must lie in (0, 1)") + k = int(np.ceil((1.0 - alpha) * (n + 1))) + if k > n: + return np.inf + return float(np.quantile(values, k / n, method="higher")) diff --git a/src/methods/base.py b/src/methods/base.py new file mode 100644 index 0000000000000000000000000000000000000000..a79a303a070b69cd05d784ef84188844bb803b43 --- /dev/null +++ b/src/methods/base.py @@ -0,0 +1,16 @@ +"""Common types for conformal methods.""" +from dataclasses import dataclass +import numpy as np + + +@dataclass +class ConformalResult: + """Standard output of any conformal method. + + covered: boolean array (n_test,), whether Y_i is in the prediction set + radius: set size surrogate (n_test,), local threshold * local scale + threshold: the conformal quantile (scalar or per-group) + """ + covered: np.ndarray + radius: np.ndarray + threshold: float | np.ndarray diff --git a/src/methods/fullcp.py b/src/methods/fullcp.py new file mode 100644 index 0000000000000000000000000000000000000000..6893817c660e8f5368604a947463a169b3b23be5 --- /dev/null +++ b/src/methods/fullcp.py @@ -0,0 +1,104 @@ +"""Symmetrized full conformal with local normalization (Theorem 4.1).""" +import numpy as np +from sklearn.neighbors import NearestNeighbors +from .base import ConformalResult +from ..utils.simplex import ilr + + +def full_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + U_cal: np.ndarray, + U_test: np.ndarray, + k: int = 20, +) -> ConformalResult: + """Symmetrized full conformal prediction with local normalization. + + For each test point j, constructs augmented set = cal ∪ {j}, computes + LOO sigma for all n_cal+1 points in the augmented set, then derives + the conformal p-value. Exact per Theorem 4.1. + + Optimization: cal-point LOO sigmas are pre-computed on the cal set and + updated only when the test point falls among a cal point's k nearest + neighbors (rare for large n_cal). + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + U_cal: calibration predictions (n_cal, K) + U_test: test predictions (n_test, K) + k: kNN neighbors for leave-one-out scale estimation + + Returns: + ConformalResult with exact marginal coverage guarantee. + """ + n_cal = len(R_cal) + n_test = len(R_test) + if n_cal < 2: + radius = np.full(n_test, np.inf, dtype=float) + return ConformalResult(covered=np.ones(n_test, dtype=bool), radius=radius, threshold=alpha) + k_loo = min(k, n_cal - 1) + + Z_cal = ilr(U_cal) + Z_test = ilr(U_test) + + # Pre-compute cal LOO sigmas (reused across test points) + nn_cal = NearestNeighbors(n_neighbors=k_loo + 1).fit(Z_cal) + cal_dists, cal_nn_idx = nn_cal.kneighbors(Z_cal) + cal_loo_sigma = np.zeros(n_cal) + for i in range(n_cal): + neighbor_idx = cal_nn_idx[i][cal_nn_idx[i] != i][:k_loo] + cal_loo_sigma[i] = max(np.median(R_cal[neighbor_idx]), 1e-8) + + # Pre-compute cal normalized scores (base, without augmentation effect) + S_cal_base = R_cal / cal_loo_sigma + + covered = np.zeros(n_test, dtype=bool) + radius = np.zeros(n_test) + + for j in range(n_test): + # For the test point: its LOO sigma is computed using cal as reference + # (in the augmented set, dropping itself leaves exactly cal) + test_dists, test_nn_idx = nn_cal.kneighbors(Z_test[j:j + 1]) + test_nbrs = test_nn_idx[0][:k_loo] + sigma_test_j = max(np.median(R_cal[test_nbrs]), 1e-8) + + # For cal points: check if adding the test point changes their LOO sigma + # A cal point's LOO sigma changes only if the test point is closer than + # its k-th nearest cal neighbor. For efficiency, compute the updated + # sigma only for affected cal points. + S_cal = S_cal_base.copy() + + # Distance from each cal point to this test point + d_to_test = np.linalg.norm(Z_cal - Z_test[j], axis=1) + # k-th neighbor distance for each cal point (last column of cal_dists, + # but we used k_loo+1 neighbors including self, so k-th non-self is index k_loo) + kth_dist = cal_dists[:, k_loo] # distance to (k_loo+1)-th neighbor (including self) + + affected = d_to_test < kth_dist + if np.any(affected): + for i in np.where(affected)[0]: + # Recompute LOO sigma: drop self from cal, add test point + orig_nbrs = cal_nn_idx[i][cal_nn_idx[i] != i][:k_loo] + # Replace the farthest neighbor with test point if closer + nbr_residuals = list(R_cal[orig_nbrs]) + # Drop the farthest cal neighbor, add test residual + nbr_residuals[-1] = R_test[j] + new_sigma = max(np.median(nbr_residuals), 1e-8) + S_cal[i] = R_cal[i] / new_sigma + + S_test_j = R_test[j] / sigma_test_j + + # p-value: fraction of augmented scores >= test score + n_geq = np.sum(S_cal >= S_test_j) + 1 # +1 for test point itself + p_val = n_geq / (n_cal + 1) + + covered[j] = p_val > alpha + # Radius: effective threshold + all_S = np.concatenate([S_cal, [S_test_j]]) + q_idx = int(np.ceil((1 - alpha) * (n_cal + 1))) - 1 + radius[j] = sigma_test_j * np.sort(all_S)[min(q_idx, len(all_S) - 1)] + + return ConformalResult(covered=covered, radius=radius, threshold=alpha) diff --git a/src/methods/global_cp.py b/src/methods/global_cp.py new file mode 100644 index 0000000000000000000000000000000000000000..0b8472ebb4bfdb31d9c94b6bad8f46bb7ddffb98 --- /dev/null +++ b/src/methods/global_cp.py @@ -0,0 +1,25 @@ +"""Global split conformal prediction.""" +import numpy as np +from .base import ConformalResult +from ._split_quantile import split_conformal_quantile + + +def global_split_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, +) -> ConformalResult: + """Standard split conformal with a single global threshold. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + + Returns: + ConformalResult + """ + q = split_conformal_quantile(R_cal, alpha) + covered = R_test <= q + radius = np.full_like(R_test, q, dtype=float) + return ConformalResult(covered=covered, radius=radius, threshold=q) diff --git a/src/methods/jackknife_plus.py b/src/methods/jackknife_plus.py new file mode 100644 index 0000000000000000000000000000000000000000..0413109019b6a2591a28c9bfc5f66ec6e5451049 --- /dev/null +++ b/src/methods/jackknife_plus.py @@ -0,0 +1,57 @@ +"""Approximate jackknife+ for simplex-valued prediction tasks. + +This implementation uses leave-one-out local score normalization on the +calibration sample as a practical surrogate when full model retraining is +unavailable. It is closer in spirit to jackknife+ than split CP, but it is +still an approximation in this project because the underlying predictor is not +re-fit for each leave-one-out fold. +""" +import numpy as np + +from ._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out +from ._split_quantile import split_conformal_quantile +from .base import ConformalResult + + +def jackknife_plus_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + U_cal: np.ndarray | None = None, + U_test: np.ndarray | None = None, + loo_scores: np.ndarray | None = None, + k: int = 20, +) -> ConformalResult: + """Approximate jackknife+ using leave-one-out calibration scores. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + U_cal: calibration predictions (n_cal, K), optional + U_test: test predictions (n_test, K), optional + loo_scores: pre-computed leave-one-out scores, optional + k: kNN neighbors for local scale estimation when U inputs are provided + + Returns: + ConformalResult with a global or locally-rescaled radius. + """ + if loo_scores is None: + if U_cal is not None and U_test is not None: + sigma_loo = knn_sigma_leave_one_out(U_cal, R_cal, k=k) + loo_scores = R_cal / sigma_loo + else: + loo_scores = np.asarray(R_cal, dtype=float) + else: + loo_scores = np.asarray(loo_scores, dtype=float) + + q = split_conformal_quantile(loo_scores, alpha) + + if U_cal is not None and U_test is not None: + sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=k) + radius = sigma_test * q + else: + radius = np.full_like(R_test, q, dtype=float) + + covered = R_test <= radius + return ConformalResult(covered=covered, radius=radius, threshold=q) diff --git a/src/methods/oneshot.py b/src/methods/oneshot.py new file mode 100644 index 0000000000000000000000000000000000000000..88f3f426c9d172f9bad7ef1f593cfea86f22cded --- /dev/null +++ b/src/methods/oneshot.py @@ -0,0 +1,38 @@ +"""One-shot kNN local scaling (no data splitting, no exchangeability guarantee).""" +import numpy as np +from .base import ConformalResult +from ._split_quantile import split_conformal_quantile +from ._knn_sigma import knn_sigma_hat + + +def oneshot_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + U_cal: np.ndarray, + U_test: np.ndarray, + k: int = 20, +) -> ConformalResult: + """One-shot locally-normalized conformal (no split for scale estimation). + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + U_cal: calibration predictions (n_cal, K) + U_test: test predictions (n_test, K) + k: kNN neighbors for scale estimation + + Returns: + ConformalResult (no formal coverage guarantee). + """ + # Estimate σ̂ from same calibration set (breaks exchangeability) + sigma_hat_cal = knn_sigma_hat(U_cal, R_cal, U_cal, k=k) + S_cal = R_cal / sigma_hat_cal + q = split_conformal_quantile(S_cal, alpha) + + sigma_hat_test = knn_sigma_hat(U_cal, R_cal, U_test, k=k) + radius = sigma_hat_test * q + covered = R_test <= radius + + return ConformalResult(covered=covered, radius=radius, threshold=q) diff --git a/src/methods/oracle.py b/src/methods/oracle.py new file mode 100644 index 0000000000000000000000000000000000000000..ea317388e27477e3c3b525f957fa4e35da21926f --- /dev/null +++ b/src/methods/oracle.py @@ -0,0 +1,30 @@ +"""Oracle local conformal: normalize by known true σ(u).""" +import numpy as np +from .base import ConformalResult + + +def oracle_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + sigma_cal: np.ndarray, + sigma_test: np.ndarray, +) -> ConformalResult: + """Conformal prediction with oracle (known) local scale. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + sigma_cal: true scale at calibration points (n_cal,) + sigma_test: true scale at test points (n_test,) + + Returns: + ConformalResult with locally-scaled radius. + """ + S_cal = R_cal / sigma_cal + n = len(S_cal) + q = np.quantile(S_cal, np.ceil((1 - alpha) * (n + 1)) / n, method="higher") + radius = sigma_test * q + covered = R_test <= radius + return ConformalResult(covered=covered, radius=radius, threshold=q) diff --git a/src/methods/partition_cp.py b/src/methods/partition_cp.py new file mode 100644 index 0000000000000000000000000000000000000000..6e37be916b9166bebf03a66341f45d57de4edcdc --- /dev/null +++ b/src/methods/partition_cp.py @@ -0,0 +1,47 @@ +"""Partition (grouped) conformal prediction.""" +import numpy as np +from .base import ConformalResult +from ._split_quantile import split_conformal_quantile + + +def partition_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + strata_cal: np.ndarray, + strata_test: np.ndarray, +) -> ConformalResult: + """Grouped conformal with per-stratum thresholds. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + strata_cal: integer group labels for cal (n_cal,) + strata_test: integer group labels for test (n_test,) + + Returns: + ConformalResult with per-group coverage guarantee. + """ + covered = np.zeros(len(R_test), dtype=bool) + radius = np.full(len(R_test), np.inf) + thresholds = {} + + for s in np.unique(strata_test): + cal_mask = strata_cal == s + test_mask = strata_test == s + R_cal_s = R_cal[cal_mask] + n_s = len(R_cal_s) + + if n_s == 0: + # No cal points in this group — cover conservatively + covered[test_mask] = True + thresholds[int(s)] = np.inf + continue + + q_s = split_conformal_quantile(R_cal_s, alpha) + covered[test_mask] = R_test[test_mask] <= q_s + radius[test_mask] = q_s + thresholds[int(s)] = float(q_s) + + return ConformalResult(covered=covered, radius=radius, threshold=thresholds) diff --git a/src/methods/trainres.py b/src/methods/trainres.py new file mode 100644 index 0000000000000000000000000000000000000000..2536798cf5f49948b25bc85db2553c142c07c4d0 --- /dev/null +++ b/src/methods/trainres.py @@ -0,0 +1,41 @@ +"""Local scaling from training residuals (expected to fail).""" +import numpy as np +from .base import ConformalResult +from ._split_quantile import split_conformal_quantile +from ._knn_sigma import knn_sigma_hat + + +def trainres_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + U_cal: np.ndarray, + U_test: np.ndarray, + R_train: np.ndarray, + U_train: np.ndarray, + k: int = 20, +) -> ConformalResult: + """Locally-normalized conformal using training residuals for scale. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + U_cal: calibration predictions (n_cal, K) + U_test: test predictions (n_test, K) + R_train: training residuals (n_train,) + U_train: training predictions (n_train, K) + k: kNN neighbors for scale estimation + + Returns: + ConformalResult (expected to be miscalibrated). + """ + sigma_hat_cal = knn_sigma_hat(U_train, R_train, U_cal, k=k) + S_cal = R_cal / sigma_hat_cal + q = split_conformal_quantile(S_cal, alpha) + + sigma_hat_test = knn_sigma_hat(U_train, R_train, U_test, k=k) + radius = sigma_hat_test * q + covered = R_test <= radius + + return ConformalResult(covered=covered, radius=radius, threshold=q) diff --git a/src/methods/twostage.py b/src/methods/twostage.py new file mode 100644 index 0000000000000000000000000000000000000000..5248e31cba86abc2685b56714327275f2b172a31 --- /dev/null +++ b/src/methods/twostage.py @@ -0,0 +1,55 @@ +"""Two-stage split conformal with local normalization (Theorem 4.2).""" +import numpy as np +from .base import ConformalResult +from ._split_quantile import split_conformal_quantile +from ._knn_sigma import knn_sigma_hat + + +def twostage_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + U_cal: np.ndarray, + U_test: np.ndarray, + n_scale_est: int | None = None, + k: int = 20, +) -> ConformalResult: + """Two-stage locally-normalized conformal prediction. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + U_cal: calibration predictions (n_cal, K) + U_test: test predictions (n_test, K) + n_scale_est: points reserved for scale estimation (default: n_cal // 2) + k: kNN neighbors for scale estimation + + Returns: + ConformalResult with exact marginal coverage guarantee. + """ + n_cal = len(R_cal) + if n_cal < 2: + radius = np.full(len(R_test), np.inf, dtype=float) + return ConformalResult(covered=np.ones(len(R_test), dtype=bool), radius=radius, threshold=np.inf) + if n_scale_est is None: + n_scale_est = n_cal // 2 + if n_scale_est <= 0 or n_scale_est >= n_cal: + raise ValueError("n_scale_est must leave at least one scale-estimation and one calibration point") + + # Stage 1: estimate σ̂ from first split + U_est, R_est = U_cal[:n_scale_est], R_cal[:n_scale_est] + U_cal2, R_cal2 = U_cal[n_scale_est:], R_cal[n_scale_est:] + + sigma_hat_cal2 = knn_sigma_hat(U_est, R_est, U_cal2, k=k) + + # Stage 2: calibrate on normalized scores + S_cal2 = R_cal2 / sigma_hat_cal2 + q = split_conformal_quantile(S_cal2, alpha) + + # Test inference + sigma_hat_test = knn_sigma_hat(U_est, R_est, U_test, k=k) + radius = sigma_hat_test * q + covered = R_test <= radius + + return ConformalResult(covered=covered, radius=radius, threshold=q) diff --git a/src/methods/weighted_cp.py b/src/methods/weighted_cp.py new file mode 100644 index 0000000000000000000000000000000000000000..05f295e427d2d5de2af1af4e37a8d9cdb08c65d5 --- /dev/null +++ b/src/methods/weighted_cp.py @@ -0,0 +1,79 @@ +"""Weighted conformal prediction under test-dependent importance weights.""" +import numpy as np + +from .base import ConformalResult + + +def _weighted_quantile_with_test_mass( + values: np.ndarray, + weights: np.ndarray, + alpha: float, + test_weight: float, +) -> float: + """Compute the weighted conformal threshold for one test point. + + The test point contributes mass at +inf, matching the weighted split + conformal recipe used under covariate shift. + """ + values = np.asarray(values, dtype=float) + weights = np.asarray(weights, dtype=float) + + order = np.argsort(values) + sorted_values = values[order] + sorted_weights = weights[order] + + total_weight = sorted_weights.sum() + test_weight + cutoff = (1.0 - alpha) * total_weight + csum = np.cumsum(sorted_weights) + idx = np.searchsorted(csum, cutoff, side="left") + if idx >= len(sorted_values): + return np.inf + return float(sorted_values[idx]) + + +def weighted_conformal( + R_cal: np.ndarray, + R_test: np.ndarray, + alpha: float, + weights_cal: np.ndarray, + weights_test: np.ndarray | None = None, +) -> ConformalResult: + """Weighted split conformal. + + Args: + R_cal: calibration residuals (n_cal,) + R_test: test residuals (n_test,) + alpha: miscoverage level + weights_cal: nonnegative calibration weights + weights_test: nonnegative test weights. If omitted, uses ones. + + Returns: + ConformalResult with test-specific weighted thresholds. + """ + weights_cal = np.asarray(weights_cal, dtype=float) + if weights_cal.shape != np.asarray(R_cal).shape: + raise ValueError("weights_cal must have the same shape as R_cal") + if not np.all(np.isfinite(weights_cal)): + raise ValueError("weights_cal must be finite") + if np.any(weights_cal < 0): + raise ValueError("weights_cal must be nonnegative") + if float(weights_cal.sum()) <= 0.0: + raise ValueError("weights_cal must have positive total mass") + + if weights_test is None: + weights_test = np.ones(len(R_test), dtype=float) + else: + weights_test = np.asarray(weights_test, dtype=float) + if weights_test.shape != np.asarray(R_test).shape: + raise ValueError("weights_test must have the same shape as R_test") + if not np.all(np.isfinite(weights_test)): + raise ValueError("weights_test must be finite") + if np.any(weights_test < 0): + raise ValueError("weights_test must be nonnegative") + + radius = np.array([ + _weighted_quantile_with_test_mass(R_cal, weights_cal, alpha, w_t) + for w_t in weights_test + ]) + covered = R_test <= radius + return ConformalResult(covered=covered, radius=radius, threshold=radius.copy()) diff --git a/src/metrics/__init__.py b/src/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..287a1e08565b656ac7a62257bbf159afc2ae0bc4 --- /dev/null +++ b/src/metrics/__init__.py @@ -0,0 +1,15 @@ +from .coverage import ( + coverage_variance, + marginal_coverage, + max_disparity, + stratified_coverage, + worst_stratum_coverage, +) +from .setsize import ( + mean_radius, + mean_volume_ratio, + radius_by_strata, + volume_ratio, + volume_ratio_by_strata, +) +from .sscv import size_stratified_coverage, size_stratified_coverage_violation diff --git a/src/metrics/coverage.py b/src/metrics/coverage.py new file mode 100644 index 0000000000000000000000000000000000000000..8234751792058b11017cb67ce7030b0c7ef9a47c --- /dev/null +++ b/src/metrics/coverage.py @@ -0,0 +1,57 @@ +"""Coverage metrics.""" +import numpy as np + + +def marginal_coverage(covered: np.ndarray) -> float: + """Overall empirical coverage.""" + return covered.mean() + + +def stratified_coverage( + covered: np.ndarray, + strata: np.ndarray, +) -> dict[int, float]: + """Stratified coverage in prediction space. + + Args: + covered: boolean array (n,) + strata: integer group labels (n,) + + Returns: + dict mapping stratum label -> empirical coverage + """ + result = {} + for s in np.unique(strata): + mask = strata == s + if mask.sum() > 0: + result[int(s)] = covered[mask].mean() + return result + + +def max_disparity( + covered: np.ndarray, + strata: np.ndarray, + alpha: float, +) -> float: + """Max |stratified_coverage - (1-alpha)| across strata.""" + sc = stratified_coverage(covered, strata) + target = 1.0 - alpha + return max(abs(v - target) for v in sc.values()) + + +def worst_stratum_coverage( + covered: np.ndarray, + strata: np.ndarray, +) -> float: + """Minimum empirical coverage across strata.""" + sc = stratified_coverage(covered, strata) + return min(sc.values()) + + +def coverage_variance( + covered: np.ndarray, + strata: np.ndarray, +) -> float: + """Variance of empirical coverage across strata.""" + sc = stratified_coverage(covered, strata) + return float(np.var(list(sc.values()))) diff --git a/src/metrics/setsize.py b/src/metrics/setsize.py new file mode 100644 index 0000000000000000000000000000000000000000..c4fef05ab7bdfb8f00b0c13f7010e27ef0c7266d --- /dev/null +++ b/src/metrics/setsize.py @@ -0,0 +1,113 @@ +"""Set size metrics.""" +import numpy as np + + +def mean_radius(radius: np.ndarray) -> float: + return radius.mean() + + +def radius_by_strata( + radius: np.ndarray, + strata: np.ndarray, +) -> dict[int, float]: + result = {} + for s in np.unique(strata): + mask = strata == s + if mask.sum() > 0: + result[int(s)] = radius[mask].mean() + return result + + +def _distance_to_center( + samples: np.ndarray, + center: np.ndarray, + score: str, +) -> np.ndarray: + if score == "aitchison": + from src.utils.simplex import aitchison_dist + + tiled_center = np.repeat(center[None, :], len(samples), axis=0) + return aitchison_dist(samples, tiled_center) + if score in {"tv", "total_variation", "l1"}: + return 0.5 * np.sum(np.abs(samples - center[None, :]), axis=1) + raise ValueError(f"Unsupported score for volume ratio: {score}") + + +def volume_ratio( + center: np.ndarray, + radius: float, + *, + score: str = "aitchison", + n_mc: int = 20000, + rng: np.random.Generator | None = None, +) -> float: + """Monte Carlo estimate of simplex volume covered by the score ball. + + The estimate is the fraction of uniformly sampled simplex points whose + distance to `center` is at most `radius`. + """ + center = np.asarray(center, dtype=float) + if rng is None: + rng = np.random.default_rng(0) + samples = rng.dirichlet(np.ones(center.shape[-1]), size=n_mc) + d = _distance_to_center(samples, center, score) + return float(np.mean(d <= radius)) + + +def mean_volume_ratio( + centers: np.ndarray, + radius: np.ndarray, + *, + score: str = "aitchison", + n_mc: int = 20000, + max_points: int | None = None, + rng: np.random.Generator | None = None, +) -> float: + """Average Monte Carlo simplex-volume ratio across test points.""" + centers = np.asarray(centers, dtype=float) + radius = np.asarray(radius, dtype=float) + if rng is None: + rng = np.random.default_rng(0) + + idx = np.arange(len(centers)) + if max_points is not None and len(idx) > max_points: + idx = rng.choice(idx, size=max_points, replace=False) + + vals = [ + volume_ratio(centers[i], radius[i], score=score, n_mc=n_mc, rng=rng) + for i in idx + ] + return float(np.mean(vals)) + + +def volume_ratio_by_strata( + centers: np.ndarray, + radius: np.ndarray, + strata: np.ndarray, + *, + score: str = "aitchison", + n_mc: int = 20000, + max_points: int | None = None, + rng: np.random.Generator | None = None, +) -> dict[int, float]: + """Average simplex-volume ratio per prediction-space stratum.""" + centers = np.asarray(centers, dtype=float) + radius = np.asarray(radius, dtype=float) + strata = np.asarray(strata) + if rng is None: + rng = np.random.default_rng(0) + + result = {} + for s in np.unique(strata): + mask = strata == s + centers_s = centers[mask] + radius_s = radius[mask] + result[int(s)] = mean_volume_ratio( + centers_s, + radius_s, + score=score, + n_mc=n_mc, + max_points=max_points, + rng=rng, + ) + return result diff --git a/src/metrics/sscv.py b/src/metrics/sscv.py new file mode 100644 index 0000000000000000000000000000000000000000..59046910bdbd4db461246547fdc5ce6db96a9ff6 --- /dev/null +++ b/src/metrics/sscv.py @@ -0,0 +1,46 @@ +"""Size-stratified coverage metrics.""" +import numpy as np + + +def size_stratified_coverage( + covered: np.ndarray, + radius: np.ndarray, + n_bins: int = 5, +) -> dict[int, float]: + """Coverage within radius/size bins. + + Bins are quantile-based to avoid empty tails when set sizes are skewed. + """ + covered = np.asarray(covered, dtype=bool) + radius = np.asarray(radius, dtype=float) + + if len(radius) == 0: + return {} + + if np.allclose(radius, radius[0]): + return {0: float(covered.mean())} + + quantiles = np.linspace(0.0, 1.0, n_bins + 1) + edges = np.unique(np.quantile(radius, quantiles)) + if len(edges) <= 1: + return {0: float(covered.mean())} + + bin_ids = np.digitize(radius, edges[1:-1], right=True) + result = {} + for b in np.unique(bin_ids): + mask = bin_ids == b + if mask.any(): + result[int(b)] = float(covered[mask].mean()) + return result + + +def size_stratified_coverage_violation( + covered: np.ndarray, + radius: np.ndarray, + alpha: float, + n_bins: int = 5, +) -> float: + """Maximum coverage deviation across prediction-size bins.""" + sc = size_stratified_coverage(covered, radius, n_bins=n_bins) + target = 1.0 - alpha + return max(abs(v - target) for v in sc.values()) diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/utils/plotting.py b/src/utils/plotting.py new file mode 100644 index 0000000000000000000000000000000000000000..c7a57c563dad84858bdda10a89236d17097bd4ec --- /dev/null +++ b/src/utils/plotting.py @@ -0,0 +1,47 @@ +"""Plotting utilities for simplex and coverage figures.""" +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.tri import Triangulation + + +def simplex_triangulation(resolution: int = 50): + """Create a triangulation grid on the 2-simplex for heatmap plotting.""" + if resolution < 2: + raise ValueError("resolution must be at least 2") + pts = [] + for i in range(resolution + 1): + for j in range(resolution + 1 - i): + a = i / resolution + b = j / resolution + c = 1.0 - a - b + pts.append((a, b, c)) + bary = np.asarray(pts) + x = bary[:, 1] + 0.5 * bary[:, 2] + y = (np.sqrt(3.0) / 2.0) * bary[:, 2] + return bary, Triangulation(x, y) + + +def plot_stratified_coverage( + results: dict, + alpha: float, + strata_labels: list[str] | None = None, + ax: plt.Axes | None = None, +): + """Bar plot of stratified coverage across methods.""" + if ax is None: + _, ax = plt.subplots(figsize=(6, 3)) + methods = list(results) + strata = sorted({s for vals in results.values() for s in vals}) + labels = strata_labels or [str(s) for s in strata] + width = 0.8 / max(len(methods), 1) + x = np.arange(len(strata)) + for offset, method in enumerate(methods): + vals = [results[method].get(s, np.nan) for s in strata] + ax.bar(x - 0.4 + width / 2 + offset * width, vals, width=width, label=method) + ax.axhline(1.0 - alpha, color="black", linewidth=1, linestyle="--", label="target") + ax.set_xticks(x) + ax.set_xticklabels(labels) + ax.set_ylabel("Coverage") + ax.set_ylim(0.0, 1.05) + ax.legend(frameon=False, fontsize=8) + return ax diff --git a/src/utils/seed.py b/src/utils/seed.py new file mode 100644 index 0000000000000000000000000000000000000000..79d781e088bf65b75155c5154f85516713532b39 --- /dev/null +++ b/src/utils/seed.py @@ -0,0 +1,6 @@ +"""Reproducibility utilities.""" +import numpy as np + + +def get_rng(seed: int) -> np.random.Generator: + return np.random.default_rng(seed) diff --git a/src/utils/simplex.py b/src/utils/simplex.py new file mode 100644 index 0000000000000000000000000000000000000000..45f54e1dd68812e16b7f7f427b65b05198706a0a --- /dev/null +++ b/src/utils/simplex.py @@ -0,0 +1,53 @@ +"""ILR transform and simplex geometry utilities.""" +import numpy as np + + +def _helmert_matrix(K: int) -> np.ndarray: + """Build (K-1, K) Helmert submatrix for ILR transform. + + Matches the forward ILR: coords[j] = sqrt((j+1)/(j+2)) * (mean(log_p[:j+1]) - log_p[j+1]) + """ + V = np.zeros((K - 1, K)) + for j in range(K - 1): + V[j, :j + 1] = np.sqrt(1.0 / ((j + 1) * (j + 2))) + V[j, j + 1] = -np.sqrt((j + 1) / (j + 2)) + return V + + +def ilr(p: np.ndarray) -> np.ndarray: + """Isometric log-ratio transform. p: (..., K) -> (..., K-1).""" + K = p.shape[-1] + V = _helmert_matrix(K) + log_p = np.log(np.clip(p, 1e-15, None)) + return log_p @ V.T # (..., K) @ (K, K-1) -> (..., K-1) + + +def ilr_inv(coords: np.ndarray, K: int | None = None) -> np.ndarray: + """Inverse ILR transform. coords: (..., K-1) -> (..., K). + + Args: + coords: ILR coordinates (..., K-1) + K: number of simplex components. If None, inferred as coords.shape[-1] + 1. + + Returns: + Simplex vectors (..., K), rows sum to 1. + """ + if K is None: + K = coords.shape[-1] + 1 + V = _helmert_matrix(K) + log_p = coords @ V # (..., K-1) @ (K-1, K) -> (..., K) + log_p -= log_p.max(axis=-1, keepdims=True) # numerical stability + p = np.exp(log_p) + return p / p.sum(axis=-1, keepdims=True) + + +def entropy(p: np.ndarray) -> np.ndarray: + """Shannon entropy of simplex vectors. p: (..., K) -> (...).""" + p_safe = np.clip(p, 1e-15, None) + return -(p_safe * np.log(p_safe)).sum(axis=-1) + + +def aitchison_dist(p: np.ndarray, q: np.ndarray) -> np.ndarray: + """Aitchison distance between simplex vectors.""" + d = ilr(p) - ilr(q) + return np.sqrt((d ** 2).sum(axis=-1)) diff --git a/src/utils/strata.py b/src/utils/strata.py new file mode 100644 index 0000000000000000000000000000000000000000..4f9cbac6a8b658a14a1e4f2e79b9a142761550f5 --- /dev/null +++ b/src/utils/strata.py @@ -0,0 +1,100 @@ +"""Prediction-space stratification utilities.""" +import numpy as np +from .simplex import entropy, ilr +from sklearn.cluster import KMeans + + +def stratify_by_entropy(U: np.ndarray, n_bins: int = 5) -> np.ndarray: + """Bin predictions by Shannon entropy.""" + H = entropy(U) + return np.digitize(H, np.linspace(H.min(), H.max(), n_bins + 1)[1:-1]) + + +def stratify_by_boundary(U: np.ndarray, n_bins: int = 5) -> np.ndarray: + """Bin predictions by proximity to simplex boundary (min component).""" + bprox = U.min(axis=-1) + return np.digitize(bprox, np.linspace(bprox.min(), bprox.max(), n_bins + 1)[1:-1]) + + +def stratify_by_kmeans(U: np.ndarray, n_clusters: int = 5, seed: int = 42) -> np.ndarray: + """K-means clustering in ILR space.""" + Z = ilr(U) + km = KMeans(n_clusters=n_clusters, random_state=seed, n_init=10) + return km.fit_predict(Z) + + +def stratify_by_argmax_group(U: np.ndarray, split_index: int = 5) -> np.ndarray: + """Two-group stratification based on whether argmax(U) is before split_index.""" + top_class = np.argmax(U, axis=1) + return (top_class >= split_index).astype(int) + + +def _quantile_edges(values: np.ndarray, n_bins: int) -> np.ndarray: + """Return stable interior quantile edges for binning a scalar score.""" + if n_bins <= 1: + return np.array([], dtype=float) + qs = np.linspace(0.0, 1.0, n_bins + 1)[1:-1] + edges = np.quantile(values, qs) + return np.unique(edges) + + +def _digitize_fixed(values: np.ndarray, n_bins: int) -> np.ndarray: + """Digitize values using globally fixed quantile edges.""" + edges = _quantile_edges(values, n_bins) + if edges.size == 0: + return np.zeros(len(values), dtype=int) + return np.digitize(values, edges) + + +def precompute_fixed_strata( + U: np.ndarray, + method: str, + n_strata: int = 5, + seed: int = 42, +) -> np.ndarray: + """Precompute a fixed stratification on a full cached prediction matrix. + + This is intended for repeated cal/test splits of the same frozen task, where + the stratification rule should remain constant across repetitions. + + Args: + U: full prediction matrix with shape (n, K). + method: one of {"entropy", "boundary", "dominant", "kmeans", "random"}. + n_strata: target number of strata. For "dominant", this is treated as + the number of grouped dominant-component bins; if it is at least K, + each dominant component gets its own stratum. + seed: random seed for methods that require stochastic initialization. + + Returns: + Integer stratum labels of shape (n,). + """ + method = method.lower() + + if method == "entropy": + return _digitize_fixed(entropy(U), n_strata) + + if method == "boundary": + return _digitize_fixed(U.min(axis=-1), n_strata) + + if method == "dominant": + top = np.argmax(U, axis=1) + k = U.shape[1] + n_groups = min(max(int(n_strata), 1), k) + if n_groups >= k: + return top.astype(int) + return np.floor(top * n_groups / k).astype(int) + + if method == "random": + z = ilr(U) + rng = np.random.default_rng(seed) + direction = rng.normal(size=z.shape[1]) + direction /= np.linalg.norm(direction) + 1e-12 + score = z @ direction + return _digitize_fixed(score, n_strata) + + if method == "kmeans": + z = ilr(U) + km = KMeans(n_clusters=n_strata, random_state=seed, n_init=10) + return km.fit_predict(z) + + raise ValueError(f"Unknown fixed stratification method: {method}") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..73a1163f2de0572b7c40562ed13a9d9f8c38077a --- /dev/null +++ b/uv.lock @@ -0,0 +1,2614 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.14" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "anndata" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "array-api-compat", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "h5py", marker = "python_full_version < '3.11'" }, + { name = "natsort", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pandas", marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/bb/895fa2e9f8cd6d1c058aa90759da715037d0f11e23713e692537555549d7/anndata-0.11.4.tar.gz", hash = "sha256:4ce08d09d2ccb5f37d32790363bbcc7fc1b79863842296ae4badfaf48c736e24", size = 541143, upload-time = "2025-03-26T11:38:54.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/4b/ab615fea52e34579d5c6c7dba86b4f9d7f3cdb6a170b348ec49f34cf4355/anndata-0.11.4-py3-none-any.whl", hash = "sha256:fefebb1480316dfa5a23924aa9f74781d447484421bb0c788b0b2ca5e3b339d2", size = 144472, upload-time = "2025-03-26T11:38:52.547Z" }, +] + +[[package]] +name = "anndata" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "array-api-compat", marker = "python_full_version >= '3.11'" }, + { name = "h5py", marker = "python_full_version >= '3.11'" }, + { name = "legacy-api-wrap", marker = "python_full_version >= '3.11'" }, + { name = "natsort", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pandas", marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "zarr", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/81/29809c5710123bb37ea9d9de9da0e83dda5be9d8419cce256e4406b37c44/anndata-0.12.10.tar.gz", hash = "sha256:73a73c99ca50400eb9dc7f2fdd400cf677ea4bb9ef1f7c04691c0fc557e43d7f", size = 2254675, upload-time = "2026-02-06T14:02:24.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/f4/4d0193dc5bab3af74e9560a8b45830d88ac707467d15ceff7e3df17adc41/anndata-0.12.10-py3-none-any.whl", hash = "sha256:e3d940d8e34373dc250f998c1011c1da52721f980de9d83a0599daa2baa286e5", size = 176574, upload-time = "2026-02-06T14:02:23.097Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "array-api-compat" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/e5/9a12dd1c2b0ad61f3c3ad0fc14b888c65fd735dd9d26805f77317303cbe5/array_api_compat-1.14.0.tar.gz", hash = "sha256:c819ba707f5c507800cb545f7e6348ff1ecc46538381d9ad9b371ffc9cd6d784", size = 106369, upload-time = "2026-02-26T12:02:42.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d3/54cd560804a8c2b898824778e86c13c2a14600bc83532a9c4f69f2f469c3/array_api_compat-1.14.0-py3-none-any.whl", hash = "sha256:ed5af1f9b6595a199c942505f281ec994892556b6efc24679a0501e87a7d6279", size = 60124, upload-time = "2026-02-26T12:02:41.127Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, +] + +[[package]] +name = "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 = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/66/0c02bd330e7d976f83fa68583d6198d76f23581bcbb5c0e98a6148f326e5/cuda_pathfinder-1.5.0-py3-none-any.whl", hash = "sha256:498f90a9e9de36044a7924742aecce11c50c49f735f1bc53e05aa46de9ea4110", size = 49739, upload-time = "2026-03-24T21:14:30.869Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "donfig" +version = "0.8.1.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fast-array-utils" +version = "1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/26/b27aca5ed2f5a13e6142bd5183defafe763c3a3fa3c64313125137e21b34/fast_array_utils-1.4.tar.gz", hash = "sha256:5c14dd5976cbadfd8eb8fe5db5c312150268be1ef8d1e2d6aac349226d023705", size = 332233, upload-time = "2026-03-27T16:08:47.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/45/9bcdb185fd05c1d20bb1228a08d99863399638dc52681cb925f8b7e0b552/fast_array_utils-1.4-py3-none-any.whl", hash = "sha256:8637de9c97ec67d53358e402486581593e5c1835b9916b60915d39bda7877f4d", size = 38249, upload-time = "2026-03-27T16:08:46.375Z" }, +] + +[package.optional-dependencies] +accel = [ + { name = "numba", marker = "python_full_version >= '3.12'" }, +] +sparse = [ + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, + { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +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/3a/6b/231413e58a787a89b316bb0d1777da3c62257e4797e09afd8d17ad3549dc/h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9", size = 3724137, upload-time = "2026-03-06T13:47:35.242Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/557ce3aad0fe8471fb5279bab0fc56ea473858a022c4ce8a0b8f303d64e9/h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3", size = 3090112, upload-time = "2026-03-06T13:47:37.634Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/e15b3d0dc8a18e56409a839e6468d6fb589bc5207c917399c2e0706eeb44/h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d", size = 4844847, upload-time = "2026-03-06T13:47:39.811Z" }, + { url = "https://files.pythonhosted.org/packages/cb/92/a8851d936547efe30cc0ce5245feac01f3ec6171f7899bc3f775c72030b3/h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d", size = 5065352, upload-time = "2026-03-06T13:47:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ae/f2adc5d0ca9626db3277a3d87516e124cbc5d0eea0bd79bc085702d04f2c/h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f", size = 4839173, upload-time = "2026-03-06T13:47:43.586Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/e0c8c69da1d8838da023a50cd3080eae5d475691f7636b35eff20bb6ef20/h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c", size = 5076216, upload-time = "2026-03-06T13:47:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/d88fd6718832133c885004c61ceeeb24dbd6397ef877dbed6b3a64d6a286/h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d", size = 3183639, upload-time = "2026-03-06T13:47:47.041Z" }, + { 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" }, +] + +[[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 = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "8.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version == '3.11.*'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*'" }, + { name = "jedi", marker = "python_full_version == '3.11.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.11.*'" }, + { name = "pexpect", marker = "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "stack-data", marker = "python_full_version == '3.11.*'" }, + { name = "traitlets", marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/09/ba70f8d662d5671687da55ad2cc0064cf795b15e1eea70907532202e7c97/ipython-9.10.1-py3-none-any.whl", hash = "sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232", size = 622827, upload-time = "2026-03-27T09:53:24.566Z" }, +] + +[[package]] +name = "ipython" +version = "9.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[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 = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "legacy-api-wrap" +version = "1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/49/f06f94048c8974205730d40beca879e43b6eee08efb0101cfb8623e60f41/legacy_api_wrap-1.5.tar.gz", hash = "sha256:b41ba6532f3ebfe3a897a35a7f97dec3be04b92a450f6c2bcf89f1b91c9cadf2", size = 11610, upload-time = "2025-11-03T13:21:12.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5b/058db09c45ba58a7321bdf2294cae651b37d6fec68117265af90cde043b0/legacy_api_wrap-1.5-py3-none-any.whl", hash = "sha256:5a8ea50e3e3bcbcdec3447b77034fd0d32cb2cf4089db799238708e4d7e0098d", size = 10182, upload-time = "2025-11-03T13:21:11.102Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, +] + +[[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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { 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" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[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 = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +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 = "numba" +version = "0.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/c9/a0fb41787d01d621046138da30f6c2100d80857bf34b3390dd68040f27a3/numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1", size = 2765679, upload-time = "2026-02-18T18:41:20.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5e/604fed821cd7e3426bb3bc99a7ed6ac0bcb489f4cd93052256437d082f95/numba-0.64.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc09b79440952e3098eeebea4bf6e8d2355fb7f12734fcd9fc5039f0dca90727", size = 2683250, upload-time = "2026-02-18T18:40:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9f/9275a723d050b5f1a9b1c7fb7dbfce324fef301a8e50c5f88338569db06c/numba-0.64.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1afe3a80b8c2f376b211fb7a49e536ef9eafc92436afc95a2f41ea5392f8cc65", size = 3742168, upload-time = "2026-02-18T18:40:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d1/97ca7dddaa36b16f4c46319bdb6b4913ba15d0245317d0d8ccde7b2d7d92/numba-0.64.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23804194b93b8cd416c6444b5fbc4956082a45fed2d25436ef49c594666e7f7e", size = 3449103, upload-time = "2026-02-18T18:40:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/b9e137ad78415373e3353564500e8bf29dbce3c0d73633bb384d4e5d7537/numba-0.64.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2a9fe998bb2cf848960b34db02c2c3b5e02cf82c07a26d9eef3494069740278", size = 2749950, upload-time = "2026-02-18T18:40:51.536Z" }, + { url = "https://files.pythonhosted.org/packages/89/a3/1a4286a1c16136c8896d8e2090d950e79b3ec626d3a8dc9620f6234d5a38/numba-0.64.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:766156ee4b8afeeb2b2e23c81307c5d19031f18d5ce76ae2c5fb1429e72fa92b", size = 2682938, upload-time = "2026-02-18T18:40:52.897Z" }, + { url = "https://files.pythonhosted.org/packages/19/16/aa6e3ba3cd45435c117d1101b278b646444ed05b7c712af631b91353f573/numba-0.64.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d17071b4ffc9d39b75d8e6c101a36f0c81b646123859898c9799cb31807c8f78", size = 3747376, upload-time = "2026-02-18T18:40:54.925Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f1/dd2f25e18d75fdf897f730b78c5a7b00cc4450f2405564dbebfaf359f21f/numba-0.64.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ead5630434133bac87fa67526eacb264535e4e9a2d5ec780e0b4fc381a7d275", size = 3453292, upload-time = "2026-02-18T18:40:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/31/29/e09d5630578a50a2b3fa154990b6b839cf95327aa0709e2d50d0b6816cd1/numba-0.64.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2b1fd93e7aaac07d6fbaed059c00679f591f2423885c206d8c1b55d65ca3f2d", size = 2749824, upload-time = "2026-02-18T18:40:58.392Z" }, + { url = "https://files.pythonhosted.org/packages/70/a6/9fc52cb4f0d5e6d8b5f4d81615bc01012e3cf24e1052a60f17a68deb8092/numba-0.64.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69440a8e8bc1a81028446f06b363e28635aa67bd51b1e498023f03b812e0ce68", size = 2683418, upload-time = "2026-02-18T18:40:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/1a74ea99b180b7a5587b0301ed1b183a2937c4b4b67f7994689b5d36fc34/numba-0.64.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13721011f693ba558b8dd4e4db7f2640462bba1b855bdc804be45bbeb55031a", size = 3804087, upload-time = "2026-02-18T18:41:01.699Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/583c647404b15f807410510fec1eb9b80cb8474165940b7749f026f21cbc/numba-0.64.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0b180b1133f2b5d8b3f09d96b6d7a9e51a7da5dda3c09e998b5bcfac85d222c", size = 3504309, upload-time = "2026-02-18T18:41:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/0fce5789b8a5035e7ace21216a468143f3144e02013252116616c58339aa/numba-0.64.0-cp312-cp312-win_amd64.whl", hash = "sha256:e63dc94023b47894849b8b106db28ccb98b49d5498b98878fac1a38f83ac007a", size = 2752740, upload-time = "2026-02-18T18:41:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/2734de90f9300a6e2503b35ee50d9599926b90cbb7ac54f9e40074cd07f1/numba-0.64.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3bab2c872194dcd985f1153b70782ec0fbbe348fffef340264eacd3a76d59fd6", size = 2683392, upload-time = "2026-02-18T18:41:06.563Z" }, + { url = "https://files.pythonhosted.org/packages/42/e8/14b5853ebefd5b37723ef365c5318a30ce0702d39057eaa8d7d76392859d/numba-0.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703a246c60832cad231d2e73c1182f25bf3cc8b699759ec8fe58a2dbc689a70c", size = 3812245, upload-time = "2026-02-18T18:41:07.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f60dc6c96d19b7185144265a5fbf01c14993d37ff4cd324b09d0212aa7ce/numba-0.64.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2e49a7900ee971d32af7609adc0cfe6aa7477c6f6cccdf6d8138538cf7756f", size = 3511328, upload-time = "2026-02-18T18:41:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/fe7003ea7e7237ee7014f8eaeeb7b0d228a2db22572ca85bab2648cf52cb/numba-0.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:396f43c3f77e78d7ec84cdfc6b04969c78f8f169351b3c4db814b97e7acf4245", size = 2752668, upload-time = "2026-02-18T18:41:11.455Z" }, +] + +[[package]] +name = "numcodecs" +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/8a391e7c356366224734efd24da929cc4796fff468bfb179fe1af6548535/numcodecs-0.16.5.tar.gz", hash = "sha256:0d0fb60852f84c0bd9543cc4d2ab9eefd37fc8efcc410acd4777e62a1d300318", size = 6276387, upload-time = "2025-11-21T02:49:48.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/85/1ac101a40ead81eaa1c7dc49a8827a30e2e436211b43ebdc63c590eb1347/numcodecs-0.16.5-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:78382dcea50622f2ef1e6e7a71dbe7f861d8fe376b27b7c297c26907304fef1e", size = 1621795, upload-time = "2025-11-21T02:49:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2d04a19cb57a3c519b4127ac377cca6471aee1990d7c18f5b1e3a4fe1306689", size = 1153030, upload-time = "2025-11-21T02:49:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/5e/41/e120ee1b390730ac5987cde2afd82e2b8442cec315ab40b94b0373e93e73/numcodecs-0.16.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c043af648eb280cd61785c99c22ff5c3c3460f906eb51a8511327c4f5111b283", size = 8510503, upload-time = "2025-11-21T02:49:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c398919ef2eb0e56b8e97456f622640bfd3deed06de3acc976989cbcb22628a3", size = 9123428, upload-time = "2025-11-21T02:49:22.328Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5b/af02c417954f46e5c7bd5163ac251f535877d909fce54861c99ae197f6f6/numcodecs-0.16.5-cp311-cp311-win_amd64.whl", hash = "sha256:3820860ed302d4d84a1c66e70981ff959d5eb712555be4e7d8ced49888594773", size = 801542, upload-time = "2025-11-21T02:49:24.265Z" }, + { url = "https://files.pythonhosted.org/packages/75/cc/55420f3641a67f78392dc0bc5d02cb9eb0a9dcebf2848d1ac77253ca61fa/numcodecs-0.16.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:24e675dc8d1550cd976a99479b87d872cb142632c75cc402fea04c08c4898523", size = 1656287, upload-time = "2025-11-21T02:49:25.755Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6c/86644987505dcb90ba6d627d6989c27bafb0699f9fd00187e06d05ea8594/numcodecs-0.16.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ddfa4341d1a3ab99989d13b01b5134abb687d3dab2ead54b450aefe4ad5bd6", size = 1148899, upload-time = "2025-11-21T02:49:26.87Z" }, + { url = "https://files.pythonhosted.org/packages/97/1e/98aaddf272552d9fef1f0296a9939d1487914a239e98678f6b20f8b0a5c8/numcodecs-0.16.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b554ab9ecf69de7ca2b6b5e8bc696bd9747559cb4dd5127bd08d7a28bec59c3a", size = 8534814, upload-time = "2025-11-21T02:49:28.547Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/78c98ef5c8b2b784453487f3e4d6c017b20747c58b470393e230c78d18e8/numcodecs-0.16.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad1a379a45bd3491deab8ae6548313946744f868c21d5340116977ea3be5b1d6", size = 9173471, upload-time = "2025-11-21T02:49:30.444Z" }, + { url = "https://files.pythonhosted.org/packages/1c/20/2fdec87fc7f8cec950d2b0bea603c12dc9f05b4966dc5924ba5a36a61bf6/numcodecs-0.16.5-cp312-cp312-win_amd64.whl", hash = "sha256:845a9857886ffe4a3172ba1c537ae5bcc01e65068c31cf1fce1a844bd1da050f", size = 801412, upload-time = "2025-11-21T02:49:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/38/38/071ced5a5fd1c85ba0e14ba721b66b053823e5176298c2f707e50bed11d9/numcodecs-0.16.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25be3a516ab677dad890760d357cfe081a371d9c0a2e9a204562318ac5969de3", size = 1654359, upload-time = "2025-11-21T02:49:33.673Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0107e839ef75b854e969cb577e140b1aadb9847893937636582d23a2a4c6ce50", size = 1144237, upload-time = "2025-11-21T02:49:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/787ea5f237b8ea7bc67140c99155f9c00b5baf11c49afc5f3bfefa298f95/numcodecs-0.16.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:015a7c859ecc2a06e2a548f64008c0ec3aaecabc26456c2c62f4278d8fc20597", size = 8483064, upload-time = "2025-11-21T02:49:36.454Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84230b4b9dad2392f2a84242bd6e3e659ac137b5a1ce3571d6965fca673e0903", size = 9126063, upload-time = "2025-11-21T02:49:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl", hash = "sha256:5088145502ad1ebf677ec47d00eb6f0fd600658217db3e0c070c321c85d6cf3d", size = 799275, upload-time = "2025-11-21T02:49:39.558Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[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 = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pynndescent" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "llvmlite" }, + { name = "numba" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "rpy2" +version = "3.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "sys_platform == 'win32'" }, + { name = "rpy2-rinterface" }, + { name = "rpy2-robjects" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/eb/8d7ddb80d7c78f017dcb170e5c3fa2f80ca6e5580fb37500c134dd344779/rpy2-3.6.7.tar.gz", hash = "sha256:f1fb4649ce7d14e935133088dec97cda27eb37ce7cc710385f81dca556fe8c9f", size = 53961, upload-time = "2026-03-27T13:35:26.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/7d/b0f3d082a0597916db7e7488ef8397a330fd0e3af1331dc651464e7bf99e/rpy2-3.6.7-py3-none-any.whl", hash = "sha256:6490e04cf8a912c0479f1803202cb8680cba702bd3d64c021601235c4fefb249", size = 9910, upload-time = "2026-03-27T13:35:25.188Z" }, +] + +[[package]] +name = "rpy2-rinterface" +version = "3.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "packaging", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/cb/b70141218d4ab442c001ca4959fc6ed3aa8edf7fd0a5db1e318225389fdb/rpy2_rinterface-3.6.6.tar.gz", hash = "sha256:a9cc1341ce5cb4df1dc67c40dc3b2ce0caface7215bd4262fe0ed011958a3369", size = 81414, upload-time = "2026-03-27T13:34:13.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/14/b443626cc21e8a332fb9d4e4db93aebfca86a2838638c12a8dbe6d140327/rpy2_rinterface-3.6.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b304599415219cc8fb11cad704334bde89835d7da6dcf4f32ac1e49c3401a596", size = 172827, upload-time = "2026-03-27T13:33:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ea/d3af4b30a3795451b8166b57d17ddcd8af239fca36d2fef399804221b758/rpy2_rinterface-3.6.6-cp310-cp310-win_amd64.whl", hash = "sha256:1ee9540a15d5e33df8535d8ac8d684c00d24be48671b0feea5dd1674a9fbf788", size = 174960, upload-time = "2026-03-27T13:33:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/b679dce77800a5d1fc1d805cab4f12ca1f1a4a881bb1a9244f71851939cc/rpy2_rinterface-3.6.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a6c91b0769871c8ce266c31a351196bf74ae62ab53602e6b7e618abb8fe123f6", size = 172821, upload-time = "2026-03-27T13:34:00.808Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5a/eb873fe4d59a2d49e397e6e66e203b8ffa24d958da411e32af983f836a2f/rpy2_rinterface-3.6.6-cp311-cp311-win_amd64.whl", hash = "sha256:4084d93c1a4e3cd89866b514d0e845e2ef38838ee06d9fd92e1643cdcb8eafd0", size = 174954, upload-time = "2026-03-27T13:34:01.927Z" }, + { url = "https://files.pythonhosted.org/packages/ea/df/4f654a504ece20caf6e9d8bf8626e85331d523010e80ab343919197c10f9/rpy2_rinterface-3.6.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bf905d3b43686c855d21b1597d4dca995c42f9ef9884a3abb1aa28af80dc9e61", size = 173140, upload-time = "2026-03-27T13:34:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/61/5e/a9f16ae0a7ea6afec0714705656525b3de43d487ce99073a6d678f23a1d1/rpy2_rinterface-3.6.6-cp312-cp312-win_amd64.whl", hash = "sha256:da2330a969b2d14b3e5bad97415935946753712b574160bf46589a9e513ec5d1", size = 175261, upload-time = "2026-03-27T13:34:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/54/66/20c4515dde1e676df87d334cdabb0255e7bf689e4a0391c9ca4d869d0ace/rpy2_rinterface-3.6.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b16c739b4dee07f72d0307281435100739fea26f59f73bf0c4c99aa069981045", size = 173138, upload-time = "2026-03-27T13:34:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/03/bf/a479b26cff3ff92aac4400454d3944e80145d4a340edc9145e6b2c327112/rpy2_rinterface-3.6.6-cp313-cp313-win_amd64.whl", hash = "sha256:3a623c71ff4e2bc0fb642837713a13d447cf9129aff11dea52362f4b08d32f59", size = 175254, upload-time = "2026-03-27T13:34:07.786Z" }, +] + +[[package]] +name = "rpy2-robjects" +version = "3.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "packaging", marker = "sys_platform == 'win32'" }, + { name = "rpy2-rinterface" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/cc/b3ad354fde3ae52241155896584c5fe759d0e204f8b7d43a347629123301/rpy2_robjects-3.6.5.tar.gz", hash = "sha256:03d099f436a01a8b51fb8f8bf602420a99c7cdda8c3fce0e3a0f532e36b4afe4", size = 105816, upload-time = "2026-03-27T13:34:48.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/0b/07c47175f6aeab70e946c68bfbe32d7e7e9044322915ade70ec5bc8a3988/rpy2_robjects-3.6.5-py3-none-any.whl", hash = "sha256:4c430e842a36a2011884910c28e4a7ddf72256e3101bcfd50cb0ec961e66bd7b", size = 125822, upload-time = "2026-03-27T13:34:47.389Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +] + +[[package]] +name = "scanpy" +version = "1.11.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "anndata", version = "0.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "anndata", version = "0.12.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "h5py", marker = "python_full_version < '3.12'" }, + { name = "joblib", marker = "python_full_version < '3.12'" }, + { name = "legacy-api-wrap", marker = "python_full_version < '3.12'" }, + { name = "matplotlib", marker = "python_full_version < '3.12'" }, + { name = "natsort", marker = "python_full_version < '3.12'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numba", marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pandas", marker = "python_full_version < '3.12'" }, + { name = "patsy", marker = "python_full_version < '3.12'" }, + { name = "pynndescent", marker = "python_full_version < '3.12'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "seaborn", marker = "python_full_version < '3.12'" }, + { name = "session-info2", marker = "python_full_version < '3.12'" }, + { name = "statsmodels", marker = "python_full_version < '3.12'" }, + { name = "tqdm", marker = "python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "umap-learn", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/a8/285f1a9c995906b7e0ae3c399208fe67cfba8126dd31359dfef0908f6edc/scanpy-1.11.5.tar.gz", hash = "sha256:b2ef5476dfb1144b7dd0fae90b0198699c7988e6b27f083904150642c7ba6b89", size = 14088122, upload-time = "2025-10-21T08:24:43.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/e9/c1d43543da87cd27e8e2a74db85cf0b6c5cff2d5f04a86bd584d2fbc2bb0/scanpy-1.11.5-py3-none-any.whl", hash = "sha256:fcd383ddcf7acbf7c0ca232c25ad51b00aec9f8d2f7c8954b8c6ee0962257166", size = 2097836, upload-time = "2025-10-21T08:24:41.741Z" }, +] + +[[package]] +name = "scanpy" +version = "1.12" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "anndata", version = "0.12.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "fast-array-utils", extra = ["accel", "sparse"], marker = "python_full_version >= '3.12'" }, + { name = "h5py", marker = "python_full_version >= '3.12'" }, + { name = "joblib", marker = "python_full_version >= '3.12'" }, + { name = "legacy-api-wrap", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib", marker = "python_full_version >= '3.12'" }, + { name = "natsort", marker = "python_full_version >= '3.12'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numba", marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pandas", marker = "python_full_version >= '3.12'" }, + { name = "patsy", marker = "python_full_version >= '3.12'" }, + { name = "pynndescent", marker = "python_full_version >= '3.12'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "seaborn", marker = "python_full_version >= '3.12'" }, + { name = "session-info2", marker = "python_full_version >= '3.12'" }, + { name = "statsmodels", marker = "python_full_version >= '3.12'" }, + { name = "tqdm", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, + { name = "umap-learn", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/180968c66be48f9dab747330beb2056df5bb7a115a56d3700da149c48916/scanpy-1.12.tar.gz", hash = "sha256:8139840bb948ce0aa0798c9b8b88c1df4f06c27641a792f0995d39cd4dcf858a", size = 14418589, upload-time = "2026-01-23T13:25:23.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/f0/000ac705a3d5b8744c6eabfce6b413b131829542ffec05020b1e931ffed4/scanpy-1.12-py3-none-any.whl", hash = "sha256:0b89827f9ba9fea8fce5a49b311e9ce34a23f922b7d8506fa845a2dc92ef0bfe", size = 2148747, upload-time = "2026-01-23T13:25:20.84Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "session-info2" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/f8/56469d9bd2cb09475cda54633660fbd2aa62f73079a1432835319f2b1a64/session_info2-0.4.tar.gz", hash = "sha256:bb01bf7c320301d6cbcfad36b85cca7befe537ffd60269be38ae9e1a956f4aa4", size = 25126, upload-time = "2026-02-07T12:05:48.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/7c/64d18f2374e19ba9bee52dee885ec81f808a1863ed0995495b83319b88bc/session_info2-0.4-py3-none-any.whl", hash = "sha256:1c06604853acd43d8cdcc9e815efbdf5534d728234e8374dea1eef29496bb1df", size = 17776, upload-time = "2026-02-07T12:05:46.878Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "simplexuq-code" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyyaml" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.optional-dependencies] +bio = [ + { name = "anndata", version = "0.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "anndata", version = "0.12.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "rpy2" }, + { name = "scanpy", version = "1.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scanpy", version = "1.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +dev = [ + { name = "ipykernel" }, + { name = "pytest" }, + { name = "ruff" }, +] +gpu = [ + { name = "torch" }, + { name = "torchvision" }, +] +r = [ + { name = "rpy2" }, +] + +[package.metadata] +requires-dist = [ + { name = "anndata", marker = "extra == 'bio'" }, + { name = "ipykernel", marker = "extra == 'dev'" }, + { name = "matplotlib", specifier = ">=3.7" }, + { name = "numpy", specifier = ">=1.24" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "rpy2", marker = "extra == 'bio'" }, + { name = "rpy2", marker = "extra == 'r'" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "scanpy", marker = "extra == 'bio'" }, + { name = "scikit-learn", specifier = ">=1.3" }, + { name = "scipy", specifier = ">=1.10" }, + { name = "torch", marker = "extra == 'gpu'", specifier = ">=2.0" }, + { name = "torchvision", marker = "extra == 'gpu'", specifier = ">=0.15" }, +] +provides-extras = ["bio", "r", "dev", "gpu"] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/6d/9ec309a175956f88eb8420ac564297f37cf9b1f73f89db74da861052dc29/statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0", size = 10142419, upload-time = "2025-12-05T19:27:35.625Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/338c5568315ec5bf3ac7cd4b71e34b98cb3b0f834919c0c04a0762f878a1/statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9", size = 10022819, upload-time = "2025-12-05T19:27:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/b0/77/5fc4cbc2d608f9b483b0675f82704a8bcd672962c379fe4d82100d388dbf/statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f", size = 10118927, upload-time = "2025-12-05T23:07:51.256Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/b86c861c32186403fe121d9ab27bc16d05839b170d92a978beb33abb995e/statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9", size = 10413015, upload-time = "2025-12-05T23:08:53.95Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/daf0dba729ccdc4176605f4a0fd5cfe71cdda671749dca10e74a732b8b1c/statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8", size = 10441248, upload-time = "2025-12-05T23:09:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1c/2e10b7c7cc44fa418272996bf0427b8016718fd62f995d9c1f7ab37adf35/statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea", size = 9583410, upload-time = "2025-12-05T19:28:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, +] + +[[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 = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, +] + +[[package]] +name = "torchvision" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, + { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "umap-learn" +version = "0.5.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pynndescent" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/9a/a1e4a257a9aa979dac4f6d5781dac929cbb0949959e2003ed82657d10b0f/umap_learn-0.5.11.tar.gz", hash = "sha256:31566ffd495fbf05d7ab3efcba703861c0f5e6fc6998a838d0e2becdd00e54f5", size = 96409, upload-time = "2026-01-12T20:44:47.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/d2/fcf7192dd1cd8c090b6cfd53fa223c4fb2887a17c47e06bc356d44f40dfb/umap_learn-0.5.11-py3-none-any.whl", hash = "sha256:cb17adbde9d544ba79481b3ab4d81ac222e940f3d9219307bea6044f869af3cc", size = 90890, upload-time = "2026-01-12T20:44:46.511Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "zarr" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "donfig", marker = "python_full_version >= '3.11'" }, + { name = "google-crc32c", marker = "python_full_version >= '3.11'" }, + { name = "numcodecs", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/5a/b8a0cf39a14c770c30bd1f2d120c54000c8cd9e84e8e79f38d9a7ce58071/zarr-3.1.6.tar.gz", hash = "sha256:d95e72cbea4b90e9a70679468b8266400331756232576ae2b43400ac5108d0eb", size = 386531, upload-time = "2026-03-23T17:25:18.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/7c/ba8ca8cbe9dbef8e83a95fc208fed8e6686c98b4719aaa0aa7f3d31fe390/zarr-3.1.6-py3-none-any.whl", hash = "sha256:b5a82c5079d1c3d4ee8f06746fa3b9a98a7d804300fa3f4be154362a33e1207e", size = 295655, upload-time = "2026-03-23T17:25:17.189Z" }, +]