From bfbf4503ae91bc5616e73ea4e4dffc243ad887f9 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Mon, 9 Mar 2026 09:18:36 +0800 Subject: [PATCH 01/29] Sparse consistency bonus matrix, ensemble alignment, NSGA-III optimizer, and benchmark infrastructure Core library changes: - Replace dense float[len_a * len_b] consistency bonus matrix with sparse per-row structure (K slots per row). Fixes integer overflow crash for large DNA families (profiles > 46k columns) and reduces memory from 10 GB to 3.2 MB for 50k x 50k case. Bit-exact identical results for all existing cases. - Add ensemble alignment with POAR consensus merging, configurable number of runs, and Hirschberg midpoint perturbation for diversity - Add alignment-guided UPGMA tree rebuild (realign) within each ensemble run - Add sequence weight rebalancing for profile merging - Add variable scoring matrix (VSM) support - Add anchor consistency bonus for progressive alignment guidance - Add detailed alignment comparison (recall/precision/F1/TC) with BAliBASE XML core block mask support - Expose new parameters through C API, CLI, and Python bindings Benchmark infrastructure: - Add NSGA-III multi-objective optimizer with pymoo mixed variables (Choice/Integer/Real) for proper categorical parameter exploration - Add benchmark datasets: BAliBASE, BRAliBASE, MDSA DNA, BaliFam100 - Add Pareto front visualization with Dash app - Validate downloaded tarballs to detect corrupt/HTML error pages - Add fallback URLs for BAliBASE downloads Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 + PRD_sparse_consistency.md | 358 ++++++ benchmarks/PRD_ensemble_optimizer.md | 257 +++++ benchmarks/PRD_kalign_align_full.md | 729 +++++++++++++ benchmarks/PRD_unified_optimizer.md | 421 ++++++++ benchmarks/datasets.py | 199 +++- benchmarks/optimize_ensemble.py | 1102 +++++++++++++++++++ benchmarks/optimize_params.py | 1181 ++++++++++++++++++++ benchmarks/optimize_unified.py | 1500 ++++++++++++++++++++++++++ benchmarks/scoring.py | 2 +- benchmarks/view_pareto.py | 999 +++++++++++++++++ lib/include/kalign/kalign.h | 31 + lib/include/kalign/kalign_config.h | 36 + lib/src/aln_mem.c | 1 - lib/src/aln_profileprofile.c | 9 +- lib/src/aln_refine.c | 10 +- lib/src/aln_run.c | 10 +- lib/src/aln_seqprofile.c | 9 +- lib/src/aln_seqseq.c | 9 +- lib/src/aln_struct.h | 5 +- lib/src/aln_wrap.c | 75 ++ lib/src/aln_wrap.h | 16 + lib/src/anchor_consistency.c | 86 +- lib/src/anchor_consistency.h | 26 +- lib/src/ensemble.c | 534 +++++++++ lib/src/ensemble.h | 23 + lib/src/msa_cmp.c | 31 + pyproject.toml | 2 +- python-kalign/_core.cpp | 164 ++- setup_server.sh | 61 ++ src/run_kalign.c | 38 +- uv.lock | 390 ++++++- 32 files changed, 8224 insertions(+), 94 deletions(-) create mode 100644 PRD_sparse_consistency.md create mode 100644 benchmarks/PRD_ensemble_optimizer.md create mode 100644 benchmarks/PRD_kalign_align_full.md create mode 100644 benchmarks/PRD_unified_optimizer.md create mode 100644 benchmarks/optimize_ensemble.py create mode 100644 benchmarks/optimize_params.py create mode 100644 benchmarks/optimize_unified.py create mode 100644 benchmarks/view_pareto.py create mode 100644 lib/include/kalign/kalign_config.h create mode 100755 setup_server.sh diff --git a/.gitignore b/.gitignore index d0d6326..c1f7678 100644 --- a/.gitignore +++ b/.gitignore @@ -419,3 +419,7 @@ callgrind.out.* *.large.fasta test_data/large/ +# Benchmark data (downloaded datasets + simulation results) +benchmarks/data/ +benchmarks/results/ + diff --git a/PRD_sparse_consistency.md b/PRD_sparse_consistency.md new file mode 100644 index 0000000..2563b8d --- /dev/null +++ b/PRD_sparse_consistency.md @@ -0,0 +1,358 @@ +# PRD: Sparse Anchor Consistency Bonus Matrix + +## 1. Problem Statement + +The anchor consistency bonus matrix is currently stored as a dense `float[len_a * len_b]` array. This causes two critical failures for large DNA/RNA families: + +**Integer overflow in DP indexing.** The DP inner loops compute `i * m->consistency_stride + j` using `int` arithmetic. When `len_a` and `len_b` both exceed ~46,340, the product overflows 32-bit `int` (46,341 × 46,341 = 2,147,488,281 > INT_MAX). The resulting negative index causes SIGBUS/segfault. + +**Excessive memory.** A 50,000 × 50,000 dense matrix requires `50000 * 50000 * 4 = 10 GB` for a single merge step. With OpenMP parallelism, multiple merges may be in flight simultaneously. + +**The matrix is extremely sparse.** Each anchor `k` contributes at most one entry per row `i`. With K anchors (typically 8), each row has at most K non-zero entries. For a 50k × 50k matrix: 400,000 non-zero entries out of 2.5 billion cells (0.016% density). + +### Affected cases + +MDSA DNA benchmark families with large profile lengths during progressive alignment: + +| Family | Seqs | Max Length | N×MaxLen | +|--------|------|-----------|----------| +| RV60_sushi_ref6 | 384 | 5,178 | 1,988,352 | +| RV60_ank_ref6 | 210 | 9,297 | 1,952,370 | +| RV40_BB40047 | 54 | 13,644 | 736,776 | +| RV40_BB40023 | 22 | 23,769 | 522,918 | +| AAA (SMART) | 427 | 1,251 | 534,177 | + +## 2. Proposed Solution + +Replace the dense `float*` bonus matrix with a per-row sparse structure: + +```c +struct sparse_bonus { + int* cols; /* cols[i * K + k] = column index, or -1 if unused */ + float* vals; /* vals[i * K + k] = bonus value */ + int n_rows; /* = len_a (number of DP rows) */ + int K; /* max entries per row (= n_anchors) */ +}; +``` + +**Memory comparison:** + +| Scenario | Dense | Sparse (K=8) | Ratio | +|----------|-------|--------------|-------| +| Protein 500×500 | 1 MB | 32 KB | 32× | +| Protein 2000×2000 | 16 MB | 128 KB | 125× | +| DNA 50000×50000 | 10 GB | 3.2 MB | 3125× | + +**DP lookup** via inline function scanning K=8 slots per cell: + +```c +static inline float sparse_bonus_lookup(const struct sparse_bonus* sb, int i, int j) +{ + float bonus = 0.0f; + const int base = i * sb->K; + for(int k = 0; k < sb->K; k++){ + if(sb->cols[base + k] < 0) break; /* early exit: unused slots at end */ + if(sb->cols[base + k] == j) + bonus += sb->vals[base + k]; + } + return bonus; +} +``` + +## 3. Detailed Design + +### 3.1 New struct and functions in `anchor_consistency.h` + +Add after `struct consistency_table`: + +```c +struct sparse_bonus { + int* cols; + float* vals; + int n_rows; + int K; +}; + +static inline float sparse_bonus_lookup(const struct sparse_bonus* sb, int i, int j) +{ + float bonus = 0.0f; + const int base = i * sb->K; + for(int k = 0; k < sb->K; k++){ + if(sb->cols[base + k] < 0) break; + if(sb->cols[base + k] == j) + bonus += sb->vals[base + k]; + } + return bonus; +} + +EXTERN void sparse_bonus_free(struct sparse_bonus* sb); +``` + +Update function signatures: + +```c +EXTERN int anchor_consistency_get_bonus(struct consistency_table* ct, + int seq_a, int len_a, + int seq_b, int len_b, + struct sparse_bonus** bonus_out); + +EXTERN int anchor_consistency_get_bonus_profile(struct consistency_table* ct, + struct msa* msa, + int node_a, int len_a, + int node_b, int len_b, + struct sparse_bonus** bonus_out); +``` + +### 3.2 Changes to `aln_struct.h` + +Add forward declaration: +```c +struct sparse_bonus; +``` + +**Remove:** +```c +float* consistency; +int consistency_stride; +``` + +**Replace with:** +```c +struct sparse_bonus* consistency; +``` + +### 3.3 Changes to `aln_mem.c` + +In `alloc_aln_mem`, change: +```c +m->consistency = NULL; +m->consistency_stride = 0; +``` +To: +```c +m->consistency = NULL; +``` + +### 3.4 Rewrite bonus construction in `anchor_consistency.c` + +#### `sparse_bonus_free`: +```c +void sparse_bonus_free(struct sparse_bonus* sb) +{ + if(sb){ + if(sb->cols) MFREE(sb->cols); + if(sb->vals) MFREE(sb->vals); + MFREE(sb); + } +} +``` + +#### `anchor_consistency_get_bonus_profile` (and `_get_bonus`): + +Replace the dense allocation: +```c +MMALLOC(bonus, sizeof(float) * len_a * len_b); +memset(bonus, 0, sizeof(float) * len_a * len_b); +``` + +With sparse allocation: +```c +struct sparse_bonus* sb = NULL; +MMALLOC(sb, sizeof(struct sparse_bonus)); +sb->cols = NULL; +sb->vals = NULL; +sb->n_rows = len_a; +sb->K = K; + +MMALLOC(sb->cols, sizeof(int) * len_a * K); +MMALLOC(sb->vals, sizeof(float) * len_a * K); +for(i = 0; i < len_a * K; i++){ + sb->cols[i] = -1; + sb->vals[i] = 0.0f; +} +``` + +Replace the dense accumulation (current line 532): +```c +bonus[i * len_b + bj] += per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; +``` + +With sparse slot insertion: +```c +{ + float val = per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; + int base = i * K; + int slot = -1; + for(int s = 0; s < K; s++){ + if(sb->cols[base + s] == bj){ slot = s; break; } /* existing entry */ + if(sb->cols[base + s] < 0){ slot = s; break; } /* empty slot */ + } + if(slot >= 0){ + sb->vals[base + slot] += val; + sb->cols[base + slot] = bj; + } +} +``` + +Note: the slot search handles both cases — accumulating into an existing entry (two anchors map the same row to the same column) and claiming a new slot. The `cols = bj` write is idempotent for existing entries. + +Return `*bonus_out = sb` instead of `*bonus_out = bonus`. + +### 3.5 Changes to DP consumer files (12 access sites) + +All 12 sites follow the same transformation. Current: +```c +if(m->consistency){ + pa += m->consistency[i * m->consistency_stride + j]; +} +``` + +Becomes: +```c +if(m->consistency){ + pa += sparse_bonus_lookup(m->consistency, ROW, COL); +} +``` + +Where ROW and COL are the same expressions currently used for `i` and `j`. + +**`aln_seqseq.c`** — 4 sites: +- Forward inner loop (line 83): ROW=`i`, COL=`j` +- Forward end-of-row (line 105): ROW=`i`, COL=`j` +- Backward inner loop (line 199): ROW=`starta + i`, COL=`j` +- Backward end-of-row (line 223): ROW=`starta + i`, COL=`j` + +**`aln_seqprofile.c`** — 4 sites: +- Forward inner loop (line 82): ROW=`i`, COL=`j` +- Forward end-of-row (line 107): ROW=`i`, COL=`j` +- Backward inner loop (line 193): ROW=`m->starta_2 + i`, COL=`j` +- Backward end-of-row (line 216): ROW=`m->starta_2 + i`, COL=`j` + +**`aln_profileprofile.c`** — 4 sites: +- Forward inner loop (line 108): ROW=`i`, COL=`j` +- Forward end-of-row (line 138): ROW=`i`, COL=`j` +- Backward inner loop (line 251): ROW=`m->starta_2 + i`, COL=`j` +- Backward end-of-row (line 280): ROW=`m->starta_2 + i`, COL=`j` + +### 3.6 Changes to `aln_run.c` + +**`do_align`** (2 locations): + +Setup (lines 259-261, 290-294): Remove `m->consistency_stride = dp_cols;` — stride is embedded in the sparse struct. + +Teardown (lines 401-405): Replace `MFREE(m->consistency)` with `sparse_bonus_free(m->consistency)`. + +**`do_align_inline_refine`** (2 locations): + +Setup (lines 566-567, 595-598): Same — remove stride assignment. + +Teardown (lines 736-740): Same — use `sparse_bonus_free`. + +## 4. Implementation Ordering + +Steps 1-2 are additive (nothing breaks). Steps 3-7 must be atomic (single commit). + +1. Add `struct sparse_bonus`, `sparse_bonus_lookup`, `sparse_bonus_free` to header/source +2. (Can be tested in isolation with a unit test) +3. Change `aln_struct.h` — replace `float* consistency` + `int consistency_stride` with `struct sparse_bonus*` +4. Update `aln_mem.c` — remove `consistency_stride` init +5. Update 12 DP access sites to use `sparse_bonus_lookup` +6. Rewrite `anchor_consistency_get_bonus` and `_get_bonus_profile` to produce sparse output +7. Update `aln_run.c` setup/teardown + +## 5. Testing Strategy + +### 5.1 Bit-exact regression (CRITICAL) + +For all BAliBASE protein families where the current implementation works correctly: +- Run alignment with current (dense) implementation, save output +- Run alignment with sparse implementation, compare output +- **Must be byte-identical** — same bonus values → same DP scores → same alignment paths + +```bash +# Save reference outputs with current code +for f in tests/data/*.tfa; do + ./build/src/kalign -i "$f" -o "/tmp/dense_$(basename $f)" --consistency 8 +done + +# After sparse implementation, compare +for f in tests/data/*.tfa; do + ./build/src/kalign -i "$f" -o "/tmp/sparse_$(basename $f)" --consistency 8 + diff "/tmp/dense_$(basename $f)" "/tmp/sparse_$(basename $f)" +done +``` + +Also run the full BAliBASE benchmark and compare F1/TC scores: +```bash +uv run python -m benchmarks.runner --dataset balibase +``` + +### 5.2 Large DNA families (new capability) + +Verify that MDSA families that crashed with dense implementation now complete: +```bash +./build-asan/src/kalign -i benchmarks/data/downloads/mdsa/unaligned/smart/AAA.afa \ + -o /tmp/test.fa --consistency 8 --realign 2 +``` + +Run the full MDSA DNA benchmark: +```bash +uv run python -m benchmarks.optimize_unified --dataset mdsa --pop-size 20 --n-gen 2 --n-workers 4 +``` + +### 5.3 Existing test suite + +All existing tests must pass unchanged: +```bash +cd build && make test # C tests +uv run pytest tests/python/ -v # Python tests +``` + +### 5.4 Performance comparison + +Time the BAliBASE benchmark with both implementations: +```bash +# Before (dense) +time uv run python -c " +from benchmarks.scoring import run_case +from benchmarks.datasets import balibase_cases +for c in balibase_cases(): + run_case(c, method='python_api', refine='none') +" + +# After (sparse) — same command +``` + +Expectation: equal or faster (better cache behavior). Any slowdown > 5% warrants investigation. + +### 5.5 Unit test for sparse_bonus + +Add a C test that: +1. Creates a `sparse_bonus` with known values +2. Verifies `sparse_bonus_lookup` returns correct values for filled slots +3. Returns 0.0f for empty slots and out-of-band columns +4. Correctly accumulates when two anchors map to the same (row, col) +5. Handles `sb == NULL` (returns 0.0f) + +## 6. Risk Analysis + +### 6.1 Incorrect accumulation +**Risk**: Two anchors mapping the same row to the same column must accumulate (add), not overwrite. +**Mitigation**: Slot-finding logic checks `cols[slot] == bj` before writing. The `+=` on vals handles accumulation. Unit test covers this case. + +### 6.2 Hirschberg sub-problem offsets +**Risk**: The divide-and-conquer DP uses `starta`, `starta_2` offsets for row indices. +**Mitigation**: The sparse lookup receives the same (row, col) coordinates as the dense indexing. No change in semantics. Bit-exact regression testing confirms correctness. + +### 6.3 Thread safety +**Risk**: Each merge step's `aln_mem` gets its own `sparse_bonus*`. The struct is read-only during DP. +**Mitigation**: Same thread-safety model as current dense implementation. No concurrent writes. + +### 6.4 Rollback strategy +The change is purely internal — no API, file format, or CLI changes. If any regression is found, revert the single commit to restore the dense implementation. + +## 7. Future Considerations + +- **Row-pointer hoisting**: Hoist `cols + i*K` and `vals + i*K` outside the j-loop for better codegen +- **SIMD lookup**: For K=8, a single AVX2 `_mm256_cmpeq_epi32` could find the matching slot in one instruction +- **Adaptive K**: If future anchor schemes use K > 16, switch to sorted columns with binary search diff --git a/benchmarks/PRD_ensemble_optimizer.md b/benchmarks/PRD_ensemble_optimizer.md new file mode 100644 index 0000000..99edd7b --- /dev/null +++ b/benchmarks/PRD_ensemble_optimizer.md @@ -0,0 +1,257 @@ +# PRD: Ensemble Parameter Optimizer + +## Goal + +Build `benchmarks/optimize_ensemble.py` — an NSGA-II optimizer that finds the best per-run parameters for kalign's ensemble alignment mode. This is the companion to `optimize_params.py` (single-run optimizer) but targets the ensemble pipeline where multiple diverse alignments are combined via POAR consensus. + +## Background + +The ensemble pipeline runs N independent alignments with different settings, then combines them using a Partial Order Alignment Representation (POAR) consensus. Currently, the per-run diversity is controlled by a hardcoded scale-factor table in `ensemble.c`. We've added `kalign_ensemble_custom()` which accepts fully independent per-run parameters, enabling proper optimization. + +Key insight from prior experiments: the consistency transform (anchor-based alignment improvement) has never been tested inside ensemble runs. It should boost the quality of each individual input alignment without reducing diversity, since diversity comes from gap penalties/matrices/noise, not from anchoring. + +## Search Space + +### Per-run parameters (× N runs) + +Each of the N runs gets its own: + +| Parameter | Range | Type | Description | +|-----------|-------|------|-------------| +| `gpo` | [2.0, 15.0] | continuous | Gap open penalty | +| `gpe` | [0.5, 5.0] | continuous | Gap extend penalty | +| `tgpe` | [0.1, 3.0] | continuous | Terminal gap extend penalty | +| `matrix` | {PFASUM43, PFASUM60, CorBLOSUM66} | discrete | Substitution matrix | +| `noise` | [0.0, 0.5] | continuous | Tree perturbation noise sigma | + += 5 parameters per run + +### Shared parameters (apply to all runs) + +| Parameter | Range | Type | Description | +|-----------|-------|------|-------------| +| `vsm_amax` | [0.0, 5.0] | continuous | Variable scoring matrix strength | +| `consistency` | {0, 1, 2, 3, 5, 8} | discrete | Anchor consistency rounds per run | +| `consistency_weight` | [0.5, 5.0] | continuous | Consistency bonus weight | +| `realign` | {0, 1, 2} | discrete | Tree-rebuild iterations per run | +| `min_support` | {0, 1, 2, ..., N} | discrete | POAR consensus threshold (0 = auto) | + += 5 shared parameters + +### Total parameter counts by N runs + +| N runs | Per-run | Shared | Total | Recommended pop_size | +|--------|---------|--------|-------|---------------------| +| 3 | 15 | 5 | 20 | 100 | +| 5 | 25 | 5 | 30 | 150 | +| 8 | 40 | 5 | 45 | 200 | + +## Architecture + +### Script: `benchmarks/optimize_ensemble.py` + +Follows the same architecture as `optimize_params.py`: + +``` +optimize_ensemble.py +├── Parameter space definition (encode/decode per-run arrays) +├── evaluate_ensemble_params() — run one ensemble config on a case list +├── evaluate_ensemble_cv() — stratified k-fold CV wrapper +├── _eval_one_ensemble() — top-level function for ProcessPoolExecutor +├── Dashboard (rich live) — same look & feel as optimize_params.py +├── EnsembleCVProblem (pymoo) — NSGA-II problem with serial/parallel eval +├── GenerationCallback — dashboard updates + checkpoint saving +├── load_checkpoint() — resume from pickle +└── main() — CLI entry point +``` + +### Key differences from `optimize_params.py` + +1. **Parameter encoding**: The decision vector has a variable-length per-run section. For N=3: `[gpo_0, gpe_0, tgpe_0, noise_0, ..., gpo_2, gpe_2, tgpe_2, noise_2, vsm_amax, consistency_weight, matrix_0, matrix_1, matrix_2, consistency, realign, min_support]`. The `decode_params()` function returns a dict with lists for per-run params. + +2. **Alignment call**: Uses `kalign._core.ensemble_custom_file_to_file()` instead of `kalign.align_file_to_file()`. + +3. **Fixed N runs**: The `--n-runs` CLI argument sets N (default: 3). Separate optimization runs for different N values, then compare Pareto fronts. + +4. **Display**: The dashboard `format_params_short()` shows per-run params compactly, e.g. `R0: gpo=7.0/PFASUM43 R1: gpo=3.5/PFASUM60 R2: gpo=10.5/CorBLOSUM66 | vsm=2.0 c=3 re=1 ms=2`. + +### CLI interface + +```bash +# Quick test +uv run python -m benchmarks.optimize_ensemble --n-runs 3 --pop-size 20 --n-gen 5 + +# Production run on Threadripper (3 runs) +uv run python -m benchmarks.optimize_ensemble \ + --n-runs 3 --pop-size 100 --n-gen 50 \ + --n-workers 56 --n-threads 1 --n-folds 5 + +# Production run (5 runs) +uv run python -m benchmarks.optimize_ensemble \ + --n-runs 5 --pop-size 150 --n-gen 60 \ + --n-workers 56 --n-threads 1 + +# Production run (8 runs) +uv run python -m benchmarks.optimize_ensemble \ + --n-runs 8 --pop-size 200 --n-gen 80 \ + --n-workers 56 --n-threads 1 + +# Resume after interrupt +uv run python -m benchmarks.optimize_ensemble \ + --resume benchmarks/results/ensemble_optim/gen_checkpoint.pkl \ + --n-gen 80 --n-workers 56 + +# Add wall time as 3rd objective +uv run python -m benchmarks.optimize_ensemble --n-runs 3 --optimize-time +``` + +### Arguments + +| Argument | Default | Description | +|----------|---------|-------------| +| `--n-runs` | 3 | Number of ensemble runs (fixed per optimization) | +| `--pop-size` | 100 | NSGA-II population size | +| `--n-gen` | 50 | Total number of generations | +| `--n-folds` | 5 | Stratified CV folds | +| `--n-threads` | 1 | OpenMP threads per alignment | +| `--n-workers` | 1 | Parallel worker processes | +| `--seed` | 42 | Random seed | +| `--optimize-time` | false | Add wall time as 3rd objective | +| `--output-dir` | `benchmarks/results/ensemble_optim` | Output directory | +| `--no-dashboard` | false | Disable rich dashboard | +| `--resume` | None | Path to checkpoint file for resume | + +## Objectives + +Same as single-run optimizer: +1. **Maximize F1** (category-averaged, held-out CV folds) +2. **Maximize TC** (category-averaged, held-out CV folds) +3. *(Optional)* **Minimize wall time** + +## Evaluation pipeline + +For each individual in the population: + +``` +decode_params(x) → per-run arrays + shared params + ↓ +for each CV fold: + for each test case: + kalign._core.ensemble_custom_file_to_file( + input, output, + run_gpo=[gpo_0, ..., gpo_N], + run_gpe=[gpe_0, ..., gpe_N], + run_tgpe=[tgpe_0, ..., tgpe_N], + run_noise=[noise_0, ..., noise_N], + run_types=[matrix_0, ..., matrix_N], + vsm_amax=vsm_amax, + realign=realign, + consistency_anchors=consistency, + consistency_weight=consistency_weight, + min_support=min_support, + refine=REFINE_CONFIDENT, # always on for ensemble + seed=42, + ) + score_alignment_detailed(reference, output) + ↓ + category-averaged F1, TC for this fold + ↓ +mean across folds → CV F1, CV TC +``` + +## Baseline + +The baseline for comparison is the current best ensemble result: +- `ens3+vsm+ref+ra1`: F1=0.768, TC=0.467 +- Uses hardcoded scale-factor table, no consistency, PFASUM43 for all runs + +This will be computed via `kalign.align_file_to_file(ensemble=3, vsm_amax=2.0, refine="confident", realign=1)` at startup. + +## Dashboard + +Same rich live dashboard as `optimize_params.py`: + +``` +┌─ Progress ──────────────────────────────────────────────────┐ +│ Gen 12/50 Eval 480/5000 Elapsed 45.2m ETA 142m │ +│ Current: R0:gpo=7.0 R1:gpo=3.5 R2:gpo=10.5 | vsm=2.0 c=3 │ +├─ Best Solutions ────────────────────────────────────────────┤ +│ Baseline: F1=0.7680 TC=0.4670 │ +│ │ +│ Best F1: 0.7823 (+0.0143) │ +│ R0:7.0/P43 R1:3.5/P60 R2:10.5/CB66 | vsm=1.8 c=3 re=1 │ +│ │ +│ Best TC: 0.4891 (+0.0221) │ +│ R0:6.5/P43 R1:4.0/P43 R2:9.0/P60 | vsm=2.1 c=5 re=1 │ +├─ Pareto Front ──────────────────────────────────────────────┤ +│ # │ CV F1 │ CV TC │ Parameters │ +│ 0 │ 0.7823 │ 0.4801 │ R0:7.0/P43 R1:3.5/P60 ... │ +│ 1 │ 0.7791 │ 0.4891 │ R0:6.5/P43 R1:4.0/P43 ... │ +│ ... │ +├─ Trend ─────────────────────────────────────────────────────┤ +│ Gen 1: F1=0.7512 Gen 5: F1=0.7634 Gen 10: F1=0.7789 │ +├─ Recent ────────────────────────────────────────────────────┤ +│ F1=0.7654 TC=0.4512 R0:8.1/P43 R1:5.2/CB66 R2:3.0/P60 │ +│ F1=0.7823 TC=0.4801 R0:7.0/P43 R1:3.5/P60 R2:10.5/CB66 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Checkpoint / Resume + +Identical to `optimize_params.py`: +- `GenerationCallback` saves `gen_checkpoint.pkl` after every completed generation +- Stores: population X and F arrays, generation count, evaluation history +- Atomic write (write to .tmp, rename) +- `--resume` loads checkpoint, reconstructs NSGA2 with saved population as initial sampling +- Remaining generations = `--n-gen` minus completed generations +- `--n-workers` and `--n-threads` can change between runs +- `--n-folds`, `--seed`, `--n-runs` must stay the same + +## Clean interrupt + +Identical to `optimize_params.py`: +- `_kill_pool()` sends SIGTERM to worker processes on KeyboardInterrupt +- `os._exit(1)` in main handler to skip atexit join-hangs +- Single Ctrl+C exits cleanly, prints checkpoint path and resume command + +## Output files + +Written to `--output-dir` (default: `benchmarks/results/ensemble_optim`): + +1. **`gen_checkpoint.pkl`** — per-generation checkpoint for resume +2. **`pareto_front.txt`** — human-readable Pareto front with full per-run parameters +3. **`optim_checkpoint.pkl`** — final results pickle (Pareto configs, history, baselines) + +## Post-optimization analysis + +After optimization completes: +1. Print Pareto front as rich table +2. Re-evaluate best-F1 solution on full dataset (all 218 cases) +3. Show per-category breakdown (RV11–RV50) comparing optimized vs baseline +4. Overfit check: flag if full-dataset score exceeds CV score by >0.02 + +## Code reuse + +The following can be imported from `optimize_params.py` or a shared module: +- `stratified_kfold()` — fold splitting logic +- `score_alignment_detailed()` — already in `scoring.py` +- `BenchmarkCase`, `balibase_cases`, etc. — already in `datasets.py` + +The following must be new (ensemble-specific): +- `decode_ensemble_params()` / `encode_ensemble_params()` +- `evaluate_ensemble_params()` — calls `ensemble_custom_file_to_file` +- `evaluate_ensemble_cv()` — CV wrapper +- `format_ensemble_params_short()` / `format_ensemble_params_long()` +- `EnsembleCVProblem` — pymoo Problem subclass +- Dashboard and callback can be adapted from the existing ones + +## Implementation order + +1. Parameter space definition + encode/decode +2. `evaluate_ensemble_params()` + `evaluate_ensemble_cv()` +3. `EnsembleCVProblem` with serial and parallel evaluation +4. Dashboard (adapt from existing) +5. `GenerationCallback` with checkpoint saving +6. `main()` with CLI, baseline, optimization loop, results +7. Resume logic +8. Smoke test with `--pop-size 4 --n-gen 2 --n-runs 3` diff --git a/benchmarks/PRD_kalign_align_full.md b/benchmarks/PRD_kalign_align_full.md new file mode 100644 index 0000000..7b324cb --- /dev/null +++ b/benchmarks/PRD_kalign_align_full.md @@ -0,0 +1,729 @@ +# PRD: Unified C Entry Point — `kalign_align_full` + +## Goal + +Replace the 7+ separate alignment entry points in kalign's C library with a single comprehensive function `kalign_align_full()` that accepts an array of per-run configs. All callers (CLI, Python bindings, benchmark optimizer) route through this one function, eliminating duplicated routing logic, silent parameter dropping, and inconsistent sentinel handling. + +## Current State: Detailed Audit + +### C Library Entry Points (`lib/src/aln_wrap.c` + `lib/src/ensemble.c`) + +#### 1. `kalign()` — aln_wrap.c line 110 + +```c +int kalign(char **seq, int *len, int numseq, int n_threads, int type, + float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len) +``` + +- **What it does**: Wraps `kalign_run()` with arr→msa→arr conversion. +- **Hardcoded**: `refine=NONE`, `adaptive_budget=0`, `quiet=1` +- **Missing**: Everything else (vsm, seq_weights, consistency, realign, ensemble, dist_scale) +- **Used by**: Legacy C API consumers only + +#### 2. `kalign_run()` — aln_wrap.c line 263 + +```c +int kalign_run(struct msa *msa, int n_threads, int type, + float gpo, float gpe, float tgpe, int refine, int adaptive_budget) +``` + +- **What it does**: Thin wrapper around `kalign_run_seeded()`. +- **Hardcoded**: `tree_seed=0, tree_noise=0.0, dist_scale=0.0, vsm_amax=-1.0, use_seq_weights=-1.0, consistency_anchors=0, consistency_weight=2.0` +- **Missing**: All advanced features +- **Used by**: `kalign()` wrapper above, and the pybind11 router as the "most basic" fallback + +#### 3. `kalign_run_seeded()` — aln_wrap.c line 133 + +```c +int kalign_run_seeded(struct msa *msa, int n_threads, int type, + float gpo, float gpe, float tgpe, + int refine, int adaptive_budget, + uint64_t tree_seed, float tree_noise, + float dist_scale, float vsm_amax, + float use_seq_weights, + int consistency_anchors, float consistency_weight) +``` + +- **What it does**: Full single-run alignment with all knobs. +- **Sentinel handling**: + - `use_seq_weights >= 0.0` → override (default is 0.0 from `aln_param_init`) + - `dist_scale > 0.0` → override (GUARDED: 0.0 means "keep default", which is also 0.0) + - `vsm_amax >= 0.0` → override (default is 2.0 for protein, 0.0 for DNA) +- **Missing**: realign iterations (separate function), ensemble +- **Consistency**: YES — builds anchor table if `consistency_anchors > 0`, frees it after refinement +- **Used by**: pybind11 router (when consistency > 0), CLI fallback, `kalign_run()`, ensemble per-run alignments + +#### 4. `kalign_run_dist_scale()` — aln_wrap.c line 268 + +```c +int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, + float gpo, float gpe, float tgpe, + int refine, int adaptive_budget, + float dist_scale, float vsm_amax, + float use_seq_weights) +``` + +- **What it does**: Like `kalign_run_seeded` but WITHOUT tree noise and WITHOUT consistency. +- **Sentinel handling**: + - `dist_scale` → UNCONDITIONAL assignment (differs from seeded!) + - `vsm_amax >= 0.0` → override + - `use_seq_weights >= 0.0` → override +- **Missing**: `tree_seed`, `tree_noise`, `consistency_anchors`, `consistency_weight` +- **BUG/GOTCHA**: No consistency support. If pybind11 router picks this path, consistency params are silently dropped. +- **Used by**: pybind11 router (when `dist_scale > 0 || vsm_amax >= 0 || seq_weights >= 0` but no consistency and no realign) + +#### 5. `kalign_run_realign()` — aln_wrap.c line 361 + +```c +int kalign_run_realign(struct msa *msa, int n_threads, int type, + float gpo, float gpe, float tgpe, + int refine, int adaptive_budget, + float dist_scale, float vsm_amax, + int realign_iterations, + float use_seq_weights, + int consistency_anchors, float consistency_weight) +``` + +- **What it does**: Full single-run + iterative tree rebuild from alignment distances. +- **Sentinel handling**: + - `dist_scale` → UNCONDITIONAL assignment + - `vsm_amax >= 0.0` → override + - `use_seq_weights >= 0.0` → override +- **Key behavior**: Builds initial tree from BPM, then after first alignment: finalise → compute pairwise distances → strip gaps → rebuild UPGMA tree → re-align. Consistency table built ONCE before first alignment (not rebuilt per iteration). +- **Missing**: `tree_seed`, `tree_noise` (always deterministic initial tree) +- **Used by**: pybind11 router (when `realign > 0`), CLI (when `realign > 0`), ensemble per-run alignments (when `realign > 0`) + +#### 6. `kalign_ensemble()` — ensemble.c line 223 + +```c +int kalign_ensemble(struct msa* msa, int n_threads, int type, + int n_runs, float gpo, float gpe, float tgpe, + uint64_t seed, int min_support, + const char* save_poar_path, + int refine, float dist_scale, float vsm_amax, + int realign, float use_seq_weights, + int consistency_anchors, float consistency_weight) +``` + +- **What it does**: N runs with hardcoded scale-factor diversity table, POAR consensus/selection. +- **Hardcoded values**: + - `use_seq_weights`: forced to 0.0 when `-1.0` (sentinel). Comment says "hurts ensemble performance". + - Per-run diversity from `run_params[]` table: 12 entries with `gpo_scale`, `gpe_scale`, `tgpe_scale`, `noise` multipliers. + - Run 0 always uses base params, no noise. + - Post-selection refinement: HARDCODES `KALIGN_REFINE_CONFIDENT` for the refinement re-run (line 421), regardless of the `refine` param passed for per-run alignments. + - Post-selection refinement: HARDCODES `dist_scale=0.0f` (line 423). + - Auto min_support when `min_support=0`: `min_sup = (n_runs + 2) / 3`, clamped to >= 2. +- **Missing**: Per-run matrix types (always uses same `type` for all runs) +- **Used by**: pybind11 router (when `ensemble > 0`), CLI (when `ensemble > 0`) + +#### 7. `kalign_ensemble_custom()` — ensemble.c line 515 + +```c +int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, + int n_runs, + const float* run_gpo, const float* run_gpe, + const float* run_tgpe, const int* run_types, + const float* run_noise, + uint64_t seed, int min_support, + int refine, float vsm_amax, + int realign, float use_seq_weights, + int consistency_anchors, float consistency_weight) +``` + +- **What it does**: Like `kalign_ensemble` but with per-run gap penalties, types, and noise. +- **Hardcoded values**: + - `use_seq_weights`: forced to 0.0 when `-1.0` (sentinel), same as `kalign_ensemble`. + - Post-selection refinement: HARDCODES `KALIGN_REFINE_CONFIDENT` (line 676). + - Post-selection refinement: HARDCODES `dist_scale=0.0f` (line 678). + - Post-selection refinement: HARDCODES `adaptive_budget=0` (line 675). + - Auto min_support: same `(n_runs + 2) / 3` formula. +- **Missing**: Per-run `dist_scale`, `vsm_amax`, `seq_weights`, `consistency` (all shared across runs). `save_poar` (removed from this function). +- **Used by**: pybind11 `ensemble_custom_file_to_file()` only (not accessible from CLI or Python wrapper) + +#### 8. `kalign_post_realign()` — aln_wrap.c line 539 + +```c +int kalign_post_realign(struct msa *msa, int n_threads, int type, + float gpo, float gpe, float tgpe, + int refine, int adaptive_budget, + float dist_scale, float vsm_amax, + int realign_iterations, + float use_seq_weights) +``` + +- **What it does**: Takes an already-finalized MSA (e.g., from ensemble) and does realign iterations. +- **Missing**: `consistency_anchors`, `consistency_weight` — no consistency support in post-realign! +- **Used by**: Was intended for post-ensemble realign, but this was found to HURT performance (0.758→0.708) so it's currently unused. + +### Routing Logic Comparison + +#### pybind11 `run_alignment()` (`_core.cpp:73-104`) + +``` +if load_poar → kalign_consensus_from_poar() +elif ensemble > 0 → kalign_ensemble() ← old hardcoded diversity, NOT ensemble_custom +elif realign > 0 → kalign_run_realign() ← OK, has consistency +elif consistency > 0 → kalign_run_seeded() ← OK +elif dist_scale > 0 → kalign_run_dist_scale() ← BUG: drops consistency! + OR vsm_amax >= 0 + OR seq_weights >= 0 +else → kalign_run() ← drops everything advanced +``` + +**Known bugs in the router**: +1. If you pass `vsm_amax=2.0` and `consistency=5`, the router picks `kalign_run_seeded` (correct, because `consistency > 0` is checked first). But if you pass `vsm_amax=2.0` and `consistency=0`, the router picks `kalign_run_dist_scale` which doesn't support consistency — OK in this case since consistency=0, but fragile logic. +2. `kalign_ensemble()` is the old hardcoded version. `ensemble_custom` is only accessible via a separate function. + +#### CLI `run_kalign()` (`src/run_kalign.c:409-464`) + +``` +if load_poar → kalign_consensus_from_poar() +elif ensemble > 0 → kalign_ensemble() ← old hardcoded diversity only +elif realign > 0 → kalign_run_realign() +else → kalign_run_seeded() ← always this for single-run +``` + +**Key differences from pybind11 router**: +1. CLI always uses `kalign_run_seeded` for non-ensemble/non-realign — never falls through to `kalign_run_dist_scale` or `kalign_run`. This means the CLI consistently handles vsm_amax and seq_weights correctly. +2. CLI has `--fast` (consistency=0) and `--precise` (ensemble=3, realign=1) modes. +3. CLI has no access to `ensemble_custom` at all. + +### Sentinel Value Summary + +| Parameter | Sentinel | Meaning | Where checked | +|-----------|----------|---------|---------------| +| `gpo` | -1.0 | Use matrix default | `aln_param_init`: `if(gpo >= 0.0)` | +| `gpe` | -1.0 | Use matrix default | `aln_param_init`: `if(gpe >= 0.0)` | +| `tgpe` | -1.0 | Use matrix default | `aln_param_init`: `if(tgpe >= 0.0)` | +| `vsm_amax` | -1.0 | Use biotype default (2.0 protein, 0.0 DNA) | `if(vsm_amax >= 0.0)` | +| `use_seq_weights` | -1.0 | Use biotype default (0.0 for all) | `if(use_seq_weights >= 0.0)` | +| `dist_scale` | 0.0 | Off | Inconsistent: guarded in seeded, unconditional in others | +| `tree_seed` | 0 | Deterministic tree | `if(tree_seed != 0 && tree_noise > 0.0f)` | +| `tree_noise` | 0.0 | No perturbation | `if(tree_seed != 0 && tree_noise > 0.0f)` | +| `consistency_anchors` | 0 | Off | `if(consistency_anchors > 0)` | +| `consistency_weight` | 2.0 | Default weight | Only used when consistency > 0 | +| `adaptive_budget` | 0 | Off | Direct flag check | +| `refine` | 0 (NONE) | No refinement | Switch/if chain | +| `realign` | 0 | No tree rebuild | `if(realign > 0)` triggers `kalign_run_realign` | + +### Inconsistencies Found + +1. **`dist_scale` sentinel handling**: In `kalign_run_seeded`, `dist_scale` is GUARDED (`if(dist_scale > 0.0f)`), so 0.0 means "keep default" (which is also 0.0 — benign). In `kalign_run_dist_scale`, `kalign_run_realign`, and `kalign_post_realign`, it's UNCONDITIONAL (`ap->dist_scale = dist_scale`). Effect: none in practice (default is 0.0), but inconsistent code. + +2. **`use_seq_weights` in ensemble**: Both `kalign_ensemble` and `kalign_ensemble_custom` force `use_seq_weights = 0.0` when the sentinel `-1.0` is passed (lines 249-250, 545-546). But if an explicit positive value is passed, it flows through. Net effect: seq_weights is always 0 in ensemble mode through all current code paths, but it's not technically blocked at the C level for explicit positive values. + +3. **Post-selection refinement mode**: In both ensemble functions, the post-selection refinement HARDCODES `KALIGN_REFINE_CONFIDENT` regardless of the `refine` parameter. The `refine` parameter is used for the per-run alignments, but the post-selection re-run always uses CONFIDENT. This is intentional but undocumented. + +4. **`kalign_run_dist_scale` drops consistency**: This function doesn't accept consistency parameters at all. The pybind11 router can fall into this path when `vsm_amax` or `seq_weights` is set but `consistency=0` and `realign=0`. Currently benign (consistency=0 means no consistency anyway), but fragile. + +5. **`ensemble_custom` missing per-run params**: `vsm_amax`, `use_seq_weights`, `consistency_anchors`, `consistency_weight`, `dist_scale` are all shared across runs in `kalign_ensemble_custom`. This is an arbitrary limitation — the optimizer would benefit from per-run control of all these. + +## Proposed Design + +### Core principle: every run is fully described by one config + +Instead of "shared params + per-run overrides", each run gets its own complete config. For ensemble, you pass an array. No ambiguity about what's shared vs per-run. + +### Per-run config struct + +```c +/* kalign_config.h */ + +/* Describes everything needed for a single alignment run. */ +struct kalign_run_config { + /* Sequence type / substitution matrix */ + int type; /* KALIGN_TYPE_* constant (UNDEFINED = auto-detect) */ + + /* Gap penalties (-1.0 = use matrix defaults) */ + float gpo; /* gap open penalty */ + float gpe; /* gap extend penalty */ + float tgpe; /* terminal gap extend penalty */ + + /* Scoring modifiers */ + float vsm_amax; /* variable scoring matrix amplitude (-1.0 = biotype default) */ + float dist_scale; /* distance-dependent gap scaling (0.0 = off) */ + float use_seq_weights; /* profile rebalancing pseudocount (-1.0 = biotype default) */ + + /* Consistency transform */ + int consistency_anchors; /* number of anchor sequences K (0 = off) */ + float consistency_weight; /* bonus scale for consistency (default: 2.0) */ + + /* Refinement */ + int refine; /* KALIGN_REFINE_* constant (default: NONE) */ + int adaptive_budget; /* scale refinement trials by uncertainty (0 = off) */ + + /* Realign (iterative tree rebuild) */ + int realign; /* number of realign iterations (0 = off) */ + + /* Guide tree perturbation */ + uint64_t tree_seed; /* random seed for noisy tree (0 = deterministic) */ + float tree_noise; /* tree perturbation sigma (0.0 = none) */ +}; +``` + +### Ensemble config struct + +```c +/* Controls orchestration when n_runs > 1. */ +struct kalign_ensemble_config { + uint64_t seed; /* base RNG seed for diversity generation */ + int min_support; /* POAR consensus threshold (0 = auto) */ + const char* save_poar; /* path to save POAR table (NULL = don't save) */ +}; +``` + +### Initialization functions + +```c +/* Returns a run config with all sentinel/default values. */ +struct kalign_run_config kalign_run_config_defaults(void); + +/* Returns an ensemble config with defaults. */ +struct kalign_ensemble_config kalign_ensemble_config_defaults(void); +``` + +Default values for `kalign_run_config_defaults()`: +```c +struct kalign_run_config kalign_run_config_defaults(void) { + struct kalign_run_config cfg; + cfg.type = KALIGN_TYPE_UNDEFINED; + cfg.gpo = -1.0f; /* sentinel: use matrix default */ + cfg.gpe = -1.0f; + cfg.tgpe = -1.0f; + cfg.vsm_amax = -1.0f; /* sentinel: use biotype default */ + cfg.dist_scale = 0.0f; /* off */ + cfg.use_seq_weights = -1.0f; /* sentinel: use biotype default */ + cfg.consistency_anchors = 0; /* off */ + cfg.consistency_weight = 2.0f; + cfg.refine = KALIGN_REFINE_NONE; + cfg.adaptive_budget = 0; + cfg.realign = 0; + cfg.tree_seed = 0; /* deterministic */ + cfg.tree_noise = 0.0f; /* no perturbation */ + return cfg; +} +``` + +Default values for `kalign_ensemble_config_defaults()`: +```c +struct kalign_ensemble_config kalign_ensemble_config_defaults(void) { + struct kalign_ensemble_config ens; + ens.seed = 42; + ens.min_support = 0; /* auto: (n_runs + 2) / 3 */ + ens.save_poar = NULL; + return ens; +} +``` + +### The unified function + +```c +int kalign_align_full( + struct msa* msa, + const struct kalign_run_config* runs, /* array of run configs */ + int n_runs, /* 1 = single-run, >1 = ensemble */ + const struct kalign_ensemble_config* ens, /* NULL when n_runs == 1 */ + int n_threads +); +``` + +### Diversity table helper + +The old `kalign_ensemble()` auto-generates per-run diversity (scaled gap penalties + tree noise) from a base config. This becomes a standalone helper that **generates** the run config array: + +```c +/* Expand one base config into n_runs configs using the built-in diversity table. + Copies base into each slot, then applies gap penalty scale factors and tree noise. + Run 0 always gets the base config unchanged. + Caller allocates out[n_runs]. */ +int kalign_generate_ensemble_runs( + const struct kalign_run_config* base, + int n_runs, + uint64_t seed, + struct kalign_run_config* out +); +``` + +### Internal logic (pseudocode) + +``` +kalign_align_full(msa, runs, n_runs, ens, n_threads): + essential_input_check(msa) + detect_alphabet(msa) + + if n_runs > 1: + // ENSEMBLE PATH + for k = 0..n_runs-1: + copy = deep_copy(msa) + resolve sentinels in runs[k] (vsm_amax, seq_weights, gpo/gpe/tgpe) + if runs[k].realign > 0: + kalign_run_realign(copy, runs[k] params...) + else: + kalign_run_seeded(copy, runs[k] params...) + extract POAR from alignment + keep alignment for scoring + + score all alignments against POAR + decide: consensus vs selection (based on ens->min_support) + if selection: post-selection refinement (REFINE_CONFIDENT) + copy winner back to msa + compute confidence from POAR + + else: + // SINGLE-RUN PATH + resolve sentinels in runs[0] + if runs[0].realign > 0: + kalign_run_realign(msa, runs[0] params...) + else: + kalign_run_seeded(msa, runs[0] params...) + + sort by original rank + return OK +``` + +## Usage Examples + +### Single run with defaults + +```c +struct kalign_run_config run = kalign_run_config_defaults(); +kalign_align_full(msa, &run, 1, NULL, 4); +``` + +### Single run with custom params + +```c +struct kalign_run_config run = kalign_run_config_defaults(); +run.gpo = 8.0f; +run.vsm_amax = 2.0f; +run.consistency_anchors = 5; +run.realign = 1; +kalign_align_full(msa, &run, 1, NULL, 4); +``` + +### Ensemble with auto-diversity (replaces `kalign_ensemble`) + +```c +struct kalign_run_config base = kalign_run_config_defaults(); +base.vsm_amax = 2.0f; +base.realign = 1; + +struct kalign_run_config runs[3]; +kalign_generate_ensemble_runs(&base, 3, 42, runs); + +struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); +kalign_align_full(msa, runs, 3, &ens, 4); +``` + +### Ensemble with fully custom per-run params (optimizer use case) + +```c +struct kalign_run_config runs[3]; + +runs[0] = kalign_run_config_defaults(); +runs[0].gpo = 8.0f; runs[0].vsm_amax = 2.0f; runs[0].tree_noise = 0.0f; + +runs[1] = kalign_run_config_defaults(); +runs[1].gpo = 6.0f; runs[1].vsm_amax = 1.5f; runs[1].tree_noise = 0.3f; +runs[1].consistency_anchors = 5; /* different consistency per run! */ + +runs[2] = kalign_run_config_defaults(); +runs[2].gpo = 10.0f; runs[2].type = KALIGN_TYPE_CORBLOSUM; +runs[2].tree_noise = 0.5f; runs[2].tree_seed = 12345; + +struct kalign_ensemble_config ens = { .seed = 42, .min_support = 0, .save_poar = NULL }; +kalign_align_full(msa, runs, 3, &ens, 4); +``` + +### CLI mode presets (using the config structs) + +```c +/* --fast */ +struct kalign_run_config run = kalign_run_config_defaults(); +run.consistency_anchors = 0; +kalign_align_full(msa, &run, 1, NULL, n_threads); + +/* default */ +struct kalign_run_config run = kalign_run_config_defaults(); +run.consistency_anchors = 5; +kalign_align_full(msa, &run, 1, NULL, n_threads); + +/* --precise */ +struct kalign_run_config base = kalign_run_config_defaults(); +base.consistency_anchors = 5; +base.realign = 1; +struct kalign_run_config runs[3]; +kalign_generate_ensemble_runs(&base, 3, 42, runs); +struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); +kalign_align_full(msa, runs, 3, &ens, n_threads); +``` + +## Python API (Option A: single function, list of dicts) + +### pybind11 binding + +```cpp +// Single Python function that handles both single-run and ensemble +m.def("align_full", [](const std::string& input_path, + const std::string& output_path, + py::list run_configs, // list of dicts + py::dict ensemble_config, // {} for single-run + int n_threads, + const std::string& format) { + int n_runs = py::len(run_configs); + std::vector runs(n_runs); + for (int i = 0; i < n_runs; i++) { + runs[i] = kalign_run_config_defaults(); + py::dict d = run_configs[i]; + if (d.contains("gpo")) runs[i].gpo = d["gpo"].cast(); + if (d.contains("vsm_amax")) runs[i].vsm_amax = d["vsm_amax"].cast(); + // ... etc for all fields, only override what's present + } + // ... build ensemble config if n_runs > 1, call kalign_align_full +}); +``` + +### Python wrapper + +```python +def align_full(input_path, output_path, *, + runs, # list of dicts, one per run + min_support=0, + ensemble_seed=42, + save_poar=None, + expand_ensemble=None, # int: generate N runs from runs[0] using diversity table + n_threads=1, + format="fasta"): + """ + Unified alignment function. + + Args: + runs: List of run config dicts. Each dict can contain: + gpo, gpe, tgpe, vsm_amax, dist_scale, use_seq_weights, + consistency_anchors, consistency_weight, refine, adaptive_budget, + realign, tree_seed, tree_noise, type + Missing keys use defaults. + expand_ensemble: If set, takes runs[0] as base and generates + this many runs using the built-in diversity table. + Ignores runs[1:] if present. + min_support: POAR consensus threshold (0 = auto). Only for ensemble. + ensemble_seed: Base RNG seed for ensemble. Only for ensemble. + """ +``` + +### Python usage examples + +**Single run:** +```python +kalign.align_full("in.fa", "out.fa", + runs=[{"vsm_amax": 2.0, "consistency_anchors": 5}]) +``` + +**Ensemble with diversity table (easy mode):** +```python +kalign.align_full("in.fa", "out.fa", + runs=[{"vsm_amax": 2.0, "realign": 1}], + expand_ensemble=3) +``` + +**Ensemble with fully custom per-run params (optimizer):** +```python +kalign.align_full("in.fa", "out.fa", + runs=[ + {"gpo": 8.0, "vsm_amax": 2.0, "tree_noise": 0.0}, + {"gpo": 6.0, "vsm_amax": 1.5, "tree_noise": 0.3, "consistency_anchors": 5}, + {"gpo": 10.0, "type": "corblosum", "tree_noise": 0.5}, + ], + min_support=0) +``` + +### Compatibility with pymoo optimizer + +The optimizer's decision vector maps directly to the array-of-runs: + +```python +def decode_to_runs(x, n_runs): + """Map pymoo decision vector → list of run config dicts.""" + DIMS_PER_RUN = 12 # gpo, gpe, tgpe, vsm_amax, dist_scale, seq_weights, + # consistency_anchors, consistency_weight, refine, + # adaptive_budget, realign, tree_noise + runs = [] + for i in range(n_runs): + off = i * DIMS_PER_RUN + runs.append({ + "gpo": x[off + 0], + "gpe": x[off + 1], + "tgpe": x[off + 2], + "vsm_amax": x[off + 3], + "dist_scale": x[off + 4], + "use_seq_weights": x[off + 5], + "consistency_anchors": int(x[off + 6]), + "consistency_weight": x[off + 7], + "refine": int(x[off + 8]), + "adaptive_budget": int(x[off + 9]), + "realign": int(x[off + 10]), + "tree_noise": x[off + 11], + }) + return runs + +def evaluate(x, input_path, n_threads): + n_runs = int(x[60]) # from ensemble block at end of vector + runs = decode_to_runs(x, n_runs) + + # Same call regardless of n_runs + kalign.align_full(input_path, output_path, + runs=runs, + min_support=int(x[61]), + n_threads=n_threads) + + return f1, tc, wall_time # three NSGA-II objectives +``` + +**Decision vector layout** (fixed size, `max_runs=5`): +``` +┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ +│ runs[0] │ runs[1] │ runs[2] │ runs[3] │ runs[4] │ ensemble │ +│ 12 dims │ 12 dims │ 12 dims │ 12 dims │ 12 dims │ 3 dims │ +└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ + ↑ masked if n_runs < 5 +``` + +Benefits for NSGA-II: +- **No shared-vs-per-run ambiguity** — each run block is self-contained +- **Run-level crossover** — swap entire 12-dim run blocks between individuals +- **Clean masking** — unused run blocks are simply ignored +- **One code path** — evaluation function doesn't branch on mode + +## What gets removed + +| Function | Action | +|----------|--------| +| `kalign_run()` | Deprecated wrapper: `cfg = defaults(); kalign_align_full(msa, &cfg, 1, NULL, n_threads)` | +| `kalign_run_seeded()` | **Keep as internal helper** — called by `kalign_align_full` for each single run | +| `kalign_run_dist_scale()` | **Remove entirely** — redundant subset of `kalign_run_seeded` | +| `kalign_run_realign()` | **Keep as internal helper** — called by `kalign_align_full` when `realign > 0` | +| `kalign_ensemble()` | Deprecated wrapper: calls `kalign_generate_ensemble_runs` + `kalign_align_full` | +| `kalign_ensemble_custom()` | Deprecated wrapper: builds run config array + `kalign_align_full` | +| `kalign_post_realign()` | **Remove** — unused, was found to hurt performance | +| `kalign()` | Deprecated wrapper: arr→msa conversion + `kalign_align_full` | + +**Important**: `kalign_run_seeded` and `kalign_run_realign` remain as internal implementation — they do the actual alignment work. `kalign_align_full` orchestrates WHEN to call them and with WHAT parameters. + +## Sentinel handling (unified) + +All sentinel resolution happens once in `kalign_align_full()`, before passing params to internal helpers: + +```c +/* For each run config: */ +for (int k = 0; k < n_runs; k++) { + resolved[k] = runs[k]; /* copy */ + + /* Resolve vsm_amax sentinel */ + if (resolved[k].vsm_amax < 0.0f) + resolved[k].vsm_amax = (biotype == PROTEIN) ? 2.0f : 0.0f; + + /* Resolve use_seq_weights sentinel */ + if (resolved[k].use_seq_weights < 0.0f) + resolved[k].use_seq_weights = 0.0f; /* biotype default is 0.0 for all */ + + /* gpo/gpe/tgpe: -1.0 sentinel passed through to aln_param_init which handles it */ + /* dist_scale: 0.0 = off, always passed through (no sentinel) */ +} +``` + +## Ensemble post-selection refinement + +Currently hardcoded to `KALIGN_REFINE_CONFIDENT` in both ensemble functions. In the unified design, this stays hardcoded with a clear comment — it's always beneficial and changing it has not been tested. If we ever want to make it configurable, it would go into `kalign_ensemble_config`. + +## Backward compatibility + +Old function signatures remain in `kalign.h` as **deprecated wrappers** that construct run configs and call `kalign_align_full()`: + +1. Existing C code that calls `kalign_run()` or `kalign_ensemble()` continues to work unchanged. +2. The wrappers can be removed in a future major version. +3. The old `kalign()` function (char** API) remains for external consumers. + +## Testing Strategy + +### Phase 1: Verify behavioral equivalence + +For each old function, create a test that: +1. Runs alignment with the old function +2. Runs the same alignment with `kalign_align_full` using equivalent config +3. Asserts the output alignments are IDENTICAL (byte-for-byte) + +Test cases from `tests/data/`: +- BB11001 (small, protein) +- BB12006 (medium, protein) +- BB30014 (protein with insertions) + +Test matrix: +``` +kalign_run(gpo=-1, gpe=-1, tgpe=-1, refine=NONE, adaptive=0) + == kalign_align_full(&defaults, 1, NULL, n_threads) + +kalign_run_seeded(gpo=8.0, gpe=1.0, tgpe=0.5, vsm_amax=2.0, seq_weights=1.0, consistency=5) + == kalign_align_full(&{same params}, 1, NULL, n_threads) + +kalign_run_dist_scale(dist_scale=0.5, vsm_amax=2.0, seq_weights=1.0) + == kalign_align_full(&{same params, consistency=0}, 1, NULL, n_threads) + +kalign_run_realign(realign=2, consistency=5, vsm_amax=2.0) + == kalign_align_full(&{same params}, 1, NULL, n_threads) + +kalign_ensemble(n_runs=3, seed=42, refine=CONFIDENT, vsm_amax=2.0, realign=1) + == kalign_generate_ensemble_runs(&base, 3, 42, runs) + kalign_align_full(runs, 3, &ens, n_threads) + +kalign_ensemble_custom(n_runs=3, run_gpo=[...], ...) + == kalign_align_full(custom_runs, 3, &ens, n_threads) +``` + +### Phase 2: Benchmark regression check + +Run BAliBASE 218 cases with: +- Old CLI path → scores +- New `kalign_align_full` path → scores +- Assert identical F1/TC (to 6 decimal places) + +### Phase 3: Python API regression + +Run existing Python test suite (`tests/python/`). All C tests + Python tests must pass. + +## Implementation Order + +1. Add `kalign_run_config`, `kalign_ensemble_config` structs and `kalign_*_defaults()` to new header `kalign_config.h` +2. Add `kalign_generate_ensemble_runs()` — extracts the existing diversity table into a config generator +3. Implement `kalign_align_full()` in `aln_wrap.c`, calling existing `kalign_run_seeded` and `kalign_run_realign` internally +4. Move ensemble orchestration from `ensemble.c` into `kalign_align_full()` (or keep as helper called by it) +5. Write equivalence tests (Phase 1) +6. Convert CLI to use `kalign_align_full()` — verify all existing tests pass +7. Convert pybind11 to single `align_full` function — verify Python tests pass +8. Run BAliBASE regression (Phase 2) +9. Add deprecated wrappers for old functions +10. Remove `kalign_run_dist_scale` and `kalign_post_realign` +11. Expose `align_full` in Python wrapper (`__init__.py`) with `expand_ensemble` convenience param + +## Files Modified + +| File | Change | +|------|--------| +| `lib/include/kalign/kalign_config.h` | **NEW**: `kalign_run_config`, `kalign_ensemble_config`, defaults functions | +| `lib/include/kalign/kalign.h` | Add `kalign_align_full()`, `kalign_generate_ensemble_runs()`. Mark old functions deprecated. | +| `lib/src/aln_wrap.c` | Implement `kalign_align_full()`. Convert old functions to wrappers. Remove `kalign_run_dist_scale`. | +| `lib/src/ensemble.c` | Extract diversity table into `kalign_generate_ensemble_runs()`. Move orchestration to `kalign_align_full()`. | +| `src/run_kalign.c` | Replace if/else chain with config struct + `kalign_align_full()` | +| `python-kalign/_core.cpp` | Replace router + `ensemble_custom_file_to_file` with single `align_full` binding | +| `python-kalign/__init__.py` | Add `align_full()` wrapper with `expand_ensemble` convenience | +| `tests/` | Add equivalence tests | + +## Risks + +1. **Ensemble diversity table**: The old `kalign_ensemble` uses a hardcoded scale-factor table. `kalign_generate_ensemble_runs` must reproduce this exactly. We keep the table as-is and just expose it as a config generator. + +2. **Post-selection refinement**: Currently hardcodes `REFINE_CONFIDENT`. Keeping this hardcoded is safe; making it configurable would need testing. + +3. **Thread safety**: `kalign_run_config` is a plain value struct with no heap pointers. `kalign_ensemble_config` has `save_poar` (read-only string) and `seed`. Multiple threads can safely use different configs. + +4. **Binary compatibility**: Adding new functions and structs is fine. Old functions become wrappers → no ABI break. Can be removed in a future major version. + +5. **Struct size stability**: If we add fields to `kalign_run_config` later, code that uses `kalign_run_config_defaults()` gets the new defaults automatically. Code that initializes with `= {0}` or partial initialization would get zeros for new fields, which should be safe (sentinels and "off" values are 0 or -1.0). diff --git a/benchmarks/PRD_unified_optimizer.md b/benchmarks/PRD_unified_optimizer.md new file mode 100644 index 0000000..7757240 --- /dev/null +++ b/benchmarks/PRD_unified_optimizer.md @@ -0,0 +1,421 @@ +# PRD: Unified Parameter Optimizer + +## Goal + +Build `benchmarks/optimize_unified.py` — a single NSGA-II optimizer that searches across kalign's entire operating range: from fast single-run alignment (no consistency, no ensemble) through consistency-enhanced single-run to multi-run ensemble with POAR consensus. The optimizer always uses three objectives: F1, TC, and wall time. The resulting 3D Pareto surface reveals the full speed/accuracy trade-off landscape in one run. + +## Motivation + +Previous optimization was split into separate scripts for single-run (`optimize_params.py`) and ensemble (`optimize_ensemble.py`), each producing isolated Pareto fronts that can't be directly compared. A unified optimizer solves this: + +1. **Direct comparability**: All configurations live on the same Pareto surface. +2. **Discovery of hybrid regimes**: The optimizer might find unexpected sweet spots. +3. **One run, one answer**: Instead of running 4+ separate optimizations and manually comparing results, one run produces the complete picture. +4. **Mandatory wall time**: Time is always the 3rd objective, so the Pareto front naturally stratifies from fast/rough to slow/accurate. + +## Complete Parameter Inventory + +This section maps every lever that exists in kalign's C code, regardless of whether it's currently exposed through the Python API. The goal is to understand the full landscape before deciding what to optimize. + +### C-level parameters (from `aln_param` struct + function signatures) + +| Parameter | C field/arg | Type | Default (protein) | Where set | Description | +|-----------|------------|------|-------------------|-----------|-------------| +| `gpo` | `ap->gpo` | float | 7.0 (P43/P60), 5.5 (CB66), 55 (GON250) | `aln_param_init` | Gap open penalty | +| `gpe` | `ap->gpe` | float | 1.25 (P43/P60), 2.0 (CB66), 8 (GON250) | `aln_param_init` | Gap extend penalty | +| `tgpe` | `ap->tgpe` | float | 1.0 (P43/P60/CB66), 4 (GON250) | `aln_param_init` | Terminal gap extend penalty | +| `subm` | `ap->subm[23][23]` | float[][] | Matrix-dependent | `aln_param_init` | Substitution matrix (selected by `type`) | +| `type` | arg to `aln_param_init` | int | `PROTEIN_PFASUM43` | caller | Which substitution matrix: PFASUM43, PFASUM60, CorBLOSUM66, GON250 | +| `vsm_amax` | `ap->vsm_amax` | float | 2.0 protein, 0.0 DNA/RNA | `aln_param_init` | Variable scoring matrix: subtracts `max(0, amax-d)` from subst scores for close pairs | +| `use_seq_weights` | `ap->use_seq_weights` | float | 0.0 | `aln_param_init` | Profile rebalancing pseudocount (0=off) | +| `dist_scale` | `ap->dist_scale` | float | 0.0 | `aln_param_init` | Distance-dependent gap scaling (0=off) | +| `consistency_anchors` | `ap->consistency_anchors` | int | 0 | `kalign_run_seeded` | Number of anchor sequences K for consistency transform (0=off) | +| `consistency_weight` | `ap->consistency_weight` | float | 2.0 | `kalign_run_seeded` | Bonus scale for consistency anchors | +| `adaptive_budget` | `ap->adaptive_budget` | int | 0 | caller | Scale refinement trial count by uncertainty (0=off, 1=on) | +| `subm_offset` | `ap->subm_offset` | float | 0.0 | computed | VSM offset, computed per alignment step (not user-settable) | +| `refine` | arg | int | NONE=0 | caller | Post-alignment refinement: NONE(0), ALL(1), CONFIDENT(2), INLINE(3) | +| `realign` | arg to `kalign_run_realign` | int | 0 | caller | Alignment-guided UPGMA tree rebuild iterations (0=off) | +| `tree_seed` | arg to `kalign_run_seeded` | uint64 | 0 | caller | Random seed for noisy guide tree (0=deterministic) | +| `tree_noise` | arg to `kalign_run_seeded` | float | 0.0 | caller | Tree perturbation noise sigma (0=none) | +| `n_runs` | arg to `kalign_ensemble*` | int | 1 | caller | Number of ensemble runs (1=single-run) | +| `min_support` | arg to ensemble | int | 0 | caller | POAR consensus threshold (0=auto selection-vs-consensus) | + +### What flows where + +``` +kalign_run_seeded(): + gpo, gpe, tgpe, type (→ subst matrix) + vsm_amax, use_seq_weights, dist_scale + consistency_anchors, consistency_weight + refine, adaptive_budget + tree_seed, tree_noise + +kalign_run_realign(): + same as kalign_run_seeded EXCEPT: + - tree_seed/tree_noise NOT supported (uses alignment-derived tree) + - adds realign_iterations + - consistency is rebuilt each iteration + +kalign_ensemble_custom(): + PER-RUN: gpo[], gpe[], tgpe[], type[], noise[] + SHARED: vsm_amax, use_seq_weights, consistency_anchors, + consistency_weight, realign, refine, min_support + NOTE: use_seq_weights IS passed through to each per-run alignment. + The -1.0 sentinel defaults to 0.0, but explicit positive values + work fine. Prior finding that seq_weights "hurts ensemble" was + with hardcoded scale-factor table — worth re-exploring. +``` + +### Parameters NOT currently in the ensemble_custom API + +| Parameter | Status | Worth adding? | +|-----------|--------|--------------| +| `dist_scale` | Hardcoded to 0.0 in ensemble_custom | Low priority — VSM serves similar purpose | +| `adaptive_budget` | Hardcoded to 0 | Low priority — minor effect | +| `refine` per-run | Currently shared across all runs | Possible but complex — would need per-run refine[] array | + +### Available substitution matrices + +| `type` constant | Name | Default gaps | Origin | Notes | +|----------------|------|-------------|--------|-------| +| `KALIGN_TYPE_PROTEIN_PFASUM43` | PFASUM43 | gpo=7.0 gpe=1.25 tgpe=1.0 | Keul et al. 2017, 43% clustering | Current default | +| `KALIGN_TYPE_PROTEIN_PFASUM60` | PFASUM60 | gpo=7.0 gpe=1.25 tgpe=1.0 | Keul et al. 2017, 60% clustering | Optimization found this is best | +| `KALIGN_TYPE_PROTEIN` | CorBLOSUM66 | gpo=5.5 gpe=2.0 tgpe=1.0 | BLOSUM66 variant | Higher entropy | +| `KALIGN_TYPE_PROTEIN_DIVERGENT` | GON250 | gpo=55 gpe=8 tgpe=4 | Gonnet 1992 | Very different scale, for divergent seqs | + +## Decisions for the Unified Optimizer + +### What to optimize + +| Parameter | Include? | Rationale | +|-----------|----------|-----------| +| `n_runs` | YES {1, 3, 5} | Core mode variable | +| `gpo_i` per-run | YES [2, 15] | Per-run diversity is key for ensemble | +| `gpe_i` per-run | YES [0.5, 5] | Per-run diversity | +| `tgpe_i` per-run | YES [0.1, 3] | Per-run diversity | +| `noise_i` per-run | YES [0, 0.5] | Tree perturbation per-run | +| `matrix_i` per-run | YES {P43, P60, CB66} | Matrix diversity per-run | +| `vsm_amax` shared | YES [0, 5] | Major effect on quality | +| `seq_weights` shared | YES [0, 5] | Works for single-run; *also* passed to per-run alignments in ensemble — re-explore | +| `consistency` shared | YES {0, 1, 2, 3, 5, 8, 10} | Huge effect, expensive | +| `consistency_weight` shared | YES [0.5, 5] | Tunes consistency strength | +| `realign` shared | YES {0, 1, 2} | Alignment-guided tree rebuild | +| `refine` shared | YES {NONE, CONFIDENT, ALL, INLINE} | Post-alignment refinement mode | +| `min_support` shared | YES {0, 1, ..., max_runs} | POAR consensus threshold | +| `dist_scale` | NO | Largely redundant with VSM, 0 extra dims | +| `adaptive_budget` | NO | Minor effect, adds noise to search | +| GON250 matrix | NO | Very different scale, would need separate gap ranges | + +### Refine as a searchable parameter + +Currently refine is hardcoded: +- Single-run: not used (NONE) +- Ensemble: REFINE_CONFIDENT for post-selection refinement + +But refine={NONE, CONFIDENT, ALL, INLINE} could be optimized. INLINE is particularly interesting — it does refinement *during* progressive alignment rather than as a post-step. This has never been benchmarked in combination with consistency or ensemble. + +Encode as discrete: {0=NONE, 1=ALL, 2=CONFIDENT, 3=INLINE} + +### Masking rules (which params are inactive when) + +The masking logic forces inactive parameters to neutral values during decode, so mutations in those dimensions are silent: + +| Condition | Masked parameters | Forced value | Why | +|-----------|-------------------|-------------|-----| +| `n_runs == 1` | `noise_0` | 0.0 | No tree perturbation in single-run path (uses deterministic tree) | +| `n_runs == 1` | `min_support` | 0 | No POAR consensus | +| `n_runs == 1` | run_1..N params | Copy of run_0 | Dead dimensions | +| `n_runs > 1` | — | — | **All shared params remain active**, including `seq_weights` | +| `consistency == 0` | `consistency_weight` | 1.0 | No anchors → weight irrelevant | +| `realign > 0` | `noise_i` | 0.0 | `kalign_run_realign` doesn't use tree_seed/noise (uses alignment-derived tree) | + +**Important**: `seq_weights` is NOT masked for ensemble mode. The C code passes `use_seq_weights` through to each per-run alignment via `kalign_run_seeded`/`kalign_run_realign`. The previous finding that "seq_weights hurts ensemble" was with the old hardcoded parameters. With optimized per-run params, seq_weights might help. The optimizer should be free to explore this. + +**Important**: When `realign > 0`, the ensemble path calls `kalign_run_realign` which builds a deterministic alignment-derived tree. Tree noise has no effect in this path. So if the optimizer picks `realign > 0`, per-run noise values should be masked to 0. However, this creates a complex interaction: `realign=0` enables noise diversity, `realign>0` gives better trees but loses noise diversity. The optimizer should discover which trade-off wins. + +### C API changes needed + +The current `ensemble_custom_file_to_file` Python binding needs one addition: + +1. **`refine` parameter**: Currently hardcoded to `REFINE_CONFIDENT` in the optimize_ensemble.py evaluation. The binding already accepts it as a parameter. We just need to make it a decision variable instead of hardcoding. + +No C code changes needed. All the levers already exist. + +## Parameter Space (final) + +### Per-run parameters (allocated for max_runs slots) + +| Parameter | Range | Type | Dims per run | +|-----------|-------|------|-------------| +| `gpo_i` | [2.0, 15.0] | continuous | 1 | +| `gpe_i` | [0.5, 5.0] | continuous | 1 | +| `tgpe_i` | [0.1, 3.0] | continuous | 1 | +| `noise_i` | [0.0, 0.5] | continuous | 1 | +| `matrix_i` | {0, 1, 2} | discrete | 1 | + += 5 per run × max_runs + +### Shared parameters + +| Parameter | Range | Type | Dims | +|-----------|-------|------|------| +| `vsm_amax` | [0.0, 5.0] | continuous | 1 | +| `seq_weights` | [0.0, 5.0] | continuous | 1 | +| `consistency_weight` | [0.5, 5.0] | continuous | 1 | +| `n_runs` | {0, 1, 2} for max5, {0, 1, 2, 3} for max8 | discrete | 1 | +| `consistency` | {0..6} → [0, 1, 2, 3, 5, 8, 10] | discrete | 1 | +| `realign` | {0, 1, 2} | discrete | 1 | +| `refine` | {0, 1, 2, 3} → [NONE, ALL, CONFIDENT, INLINE] | discrete | 1 | +| `min_support` | {0..max_runs} | discrete | 1 | + += 8 shared (3 continuous + 5 discrete) + +### Total dimensions + +| max_runs | Per-run | Shared | Total | Recommended pop_size | +|----------|---------|--------|-------|---------------------| +| 5 | 25 | 8 | 33 | 200 | +| 8 | 40 | 8 | 48 | 300 | + +## Evaluation Pipeline + +### Decision routing + +```python +def evaluate_unified(params, cases, n_threads=1, quiet=True): + n_runs = params["n_runs"] + + if n_runs == 1: + # Single-run path — full parameter control + kalign.align_file_to_file( + input, output, + gap_open=params["gpo"][0], + gap_extend=params["gpe"][0], + terminal_gap_extend=params["tgpe"][0], + seq_type=matrix_api_name(params["matrix"][0]), + vsm_amax=params["vsm_amax"], + seq_weights=params["seq_weights"], + consistency=params["consistency"], + consistency_weight=params["consistency_weight"], + realign=params["realign"], + refine=refine_api_name(params["refine"]), + ) + else: + # Ensemble path — per-run arrays + ensemble_custom_file_to_file( + input, output, + run_gpo=params["gpo"][:n_runs], + run_gpe=params["gpe"][:n_runs], + run_tgpe=params["tgpe"][:n_runs], + run_noise=params["noise"][:n_runs], + run_types=params["matrix"][:n_runs], + vsm_amax=params["vsm_amax"], + seq_weights=params["seq_weights"], # NOT forced to 0! + realign=params["realign"], + consistency_anchors=params["consistency"], + consistency_weight=params["consistency_weight"], + refine=params["refine"], + min_support=params["min_support"], + seed=42, + ) +``` + +### CV evaluation + +Same as existing: stratified k-fold (default 5) on BAliBASE 218 cases. + +## Objectives (always 3) + +1. **Maximize F1** — category-averaged, held-out CV folds → pymoo minimizes -F1 +2. **Maximize TC** — category-averaged, held-out CV folds → pymoo minimizes -TC +3. **Minimize wall time** — total CV evaluation time in seconds + +No `--optimize-time` flag. Wall time is always the 3rd objective. + +## Baselines + +Computed at startup (parallelized across folds + workers): + +| Baseline | Description | Expected profile | +|----------|-------------|-----------------| +| **fast** | n_runs=1, consistency=0, realign=0, refine=NONE, PFASUM60, default gaps | F1~0.72, TC~0.47, ~10s | +| **accurate** | n_runs=1, consistency=8, realign=2, refine=NONE, optimized gaps from run 1 | F1~0.76, TC~0.47, ~180s | +| **ensemble** | n_runs=3, consistency=0, realign=1, refine=CONFIDENT, vsm=2.0 | F1~0.77, TC~0.47, ~193s | + +## Parallelism + +### Fine-grained (individual x fold) job distribution + +Same as updated `optimize_params.py`: + +- Each job = one (individual, fold) pair, running ~44 cases +- For pop=200, k=5 → 1000 jobs per generation +- Distributed across N workers + +### Baseline parallelization + +All baseline evaluations (3 baselines × (k folds + 1 full)) run in parallel at startup. + +### Top-level function + +```python +def _eval_one_unified_fold(args_tuple): + ind_idx, fold_idx, x, test_cases, n_threads, max_runs = args_tuple + params = decode_unified_params(x, max_runs) + result = evaluate_unified(params, test_cases, n_threads, quiet=True) + return ind_idx, fold_idx, params, result +``` + +## Dashboard + +### Layout + +``` +┌─ Progress ──────────────────────────────────────────────────────┐ +│ Gen 12/100 Eval 480/20000 Elapsed 2.1h ETA 5.3h │ +│ Gen time 128s Workers 56 │ +├─ Baselines ─────────────────────────────────────────────────────┤ +│ fast: F1=0.7160 TC=0.4660 Time=10s (single, c=0) │ +│ accurate: F1=0.7557 TC=0.4713 Time=180s (single, c=8) │ +│ ensemble: F1=0.7680 TC=0.4670 Time=193s (ens3, c=0) │ +├─ Best by Objective ─────────────────────────────────────────────┤ +│ Best F1: 0.7823 (+0.0143 vs accurate) ens3 c=5 re=1 210s │ +│ Best TC: 0.5102 (+0.0389 vs accurate) ens5 c=3 re=2 340s │ +│ Fastest: 8.2s (-1.8s vs fast) single c=0 re=0 │ +├─ Pareto Front (top 12 by F1) ──────────────────────────────────┤ +│ # │ Mode │ CV F1 │ CV TC │ Time │ c re ref│ Key params │ +│ 0 │ ens5 │ 0.7823 │ 0.5001 │ 340s │ 5 1 C │ vsm=1.4 │ +│ 1 │ ens3 │ 0.7791 │ 0.4891 │ 210s │ 3 1 C │ vsm=1.8 │ +│ 2 │ ens3 │ 0.7756 │ 0.4812 │ 155s │ 0 1 C │ vsm=2.0 │ +│ 3 │ single│ 0.7557 │ 0.4713 │ 180s │ 8 2 N │ vsm=1.4 P60 │ +│ 4 │ single│ 0.7401 │ 0.4650 │ 42s │ 3 1 N │ vsm=2.0 P60 │ +│ 5 │ single│ 0.7160 │ 0.4580 │ 9s │ 0 0 N │ vsm=1.8 P60 │ +├─ Mode Distribution ────────────────────────────────────────────┤ +│ Pareto: single=8 ens3=12 ens5=5 │ +│ Pop: single=65 ens3=82 ens5=53 │ +├─ Trend ────────────────────────────────────────────────────────┤ +│ Gen 1: F1=0.7012 Gen 5: F1=0.7434 Gen 10: F1=0.7689 │ +├─ Recent ───────────────────────────────────────────────────────┤ +│ F1=0.7654 TC=0.4512 t=145s ens3 c=3 re=1 ref=C vsm=1.8 │ +│ F1=0.7201 TC=0.4380 t=12s single c=0 re=0 ref=N vsm=2.1 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key dashboard features + +1. **Mode column**: "single", "ens3", "ens5", "ens8" — most important column. +2. **Refine column**: N=None, A=All, C=Confident, I=Inline — compact 1-char code. +3. **Mode distribution panel**: Pareto and population counts per mode. +4. **Three baselines**: Context for all three speed regimes. +5. **Time always shown**: Every entry shows wall time. +6. **Compact params**: For ensemble, show shared params only (c, re, ref, vsm, sw). Full per-run details go to output file. + +## CLI Interface + +```bash +# Quick smoke test +uv run python -m benchmarks.optimize_unified --pop-size 20 --n-gen 5 + +# Production run on Threadripper (max 5 ensemble runs) +uv run python -m benchmarks.optimize_unified \ + --pop-size 200 --n-gen 100 \ + --n-workers 56 --n-threads 1 + +# Larger search (max 8 ensemble runs) +uv run python -m benchmarks.optimize_unified \ + --max-runs 8 --pop-size 300 --n-gen 120 \ + --n-workers 56 + +# Resume +uv run python -m benchmarks.optimize_unified \ + --resume benchmarks/results/unified_optim/gen_checkpoint.pkl \ + --n-gen 150 --n-workers 56 +``` + +### Arguments + +| Argument | Default | Description | +|----------|---------|-------------| +| `--max-runs` | 5 | Max ensemble runs. Sets n_runs choices: {1,3,5} or {1,3,5,8} | +| `--pop-size` | 200 | NSGA-II population size | +| `--n-gen` | 100 | Total generations | +| `--n-folds` | 5 | Stratified CV folds | +| `--n-threads` | 1 | OpenMP threads per alignment | +| `--n-workers` | 1 | Parallel worker processes | +| `--seed` | 42 | Random seed | +| `--output-dir` | `benchmarks/results/unified_optim` | Output directory | +| `--run-name` | None | Subdirectory name | +| `--no-dashboard` | false | Disable rich dashboard | +| `--resume` | None | Checkpoint file path | + +## Checkpoint / Resume + +Same pattern as existing optimizers: +- Saves `pop_X`, `pop_F`, `n_gen_completed`, history, plus `max_runs` for validation +- Atomic write via temp file + rename +- On resume: validates `max_runs`, `n_folds`, `seed` match + +## Clean Interrupt + +- `_kill_pool()` sends SIGTERM to workers +- `os._exit(1)` to skip atexit join-hangs + +## Output Files + +1. **`gen_checkpoint.pkl`** — per-generation checkpoint +2. **`pareto_front.txt`** — human-readable with full per-run params for ensemble solutions +3. **`optim_results.pkl`** — full results pickle + +### pareto_front.txt format + +``` +# Unified kalign optimization (NSGA-II, 3 objectives: F1, TC, time) +# pop_size=200 n_gen=100 max_runs=5 n_folds=5 seed=42 +# Baselines: +# fast: F1=0.7160 TC=0.4660 Time=10s +# accurate: F1=0.7557 TC=0.4713 Time=180s +# ensemble: F1=0.7680 TC=0.4670 Time=193s + +[0] mode=ens5 CV_F1=0.7823 CV_TC=0.5001 Time=340s + n_runs=5 + vsm_amax=1.359 seq_weights=0.8 + consistency=5 consistency_weight=1.17 + realign=1 refine=CONFIDENT + min_support=2 + run_0: gpo=7.0 gpe=0.55 tgpe=0.41 noise=0.10 PFASUM60 + run_1: gpo=3.5 gpe=1.20 tgpe=0.80 noise=0.25 PFASUM43 + ... + +[1] mode=single CV_F1=0.7557 CV_TC=0.4713 Time=180s + n_runs=1 + gap_open=8.472 gap_extend=0.554 terminal_gap_extend=0.409 + vsm_amax=1.359 seq_weights=3.407 + consistency=8 consistency_weight=1.167 + realign=2 refine=NONE + matrix=PFASUM60 +``` + +## Post-Optimization Analysis + +1. Full Pareto front as rich table +2. Group by mode, show best F1/TC/time per mode +3. Re-evaluate top-3 on full dataset +4. Per-category breakdown (RV11-RV50) +5. Overfit check (full > CV by >0.02) +6. Print recommended settings for three tiers: + - **Fast** (best F1 among solutions under 15s) + - **Default** (best F1 among solutions under 60s) + - **Accurate** (best F1 overall) + +## Implementation Order + +1. Parameter space definition + encode/decode with masking +2. `evaluate_unified()` with single-run / ensemble routing +3. `_eval_one_unified_fold()` for parallel eval +4. `UnifiedCVProblem` with 3 mandatory objectives +5. Dashboard with mode columns, multiple baselines, mode distribution +6. `GenerationCallback` with checkpoint saving +7. `main()` with CLI, parallel baselines, optimization loop, results +8. Resume logic with max_runs validation +9. Post-optimization analysis +10. Smoke test: `--pop-size 10 --n-gen 3 --max-runs 5 --n-workers 4` diff --git a/benchmarks/datasets.py b/benchmarks/datasets.py index da7aca2..5434fe1 100644 --- a/benchmarks/datasets.py +++ b/benchmarks/datasets.py @@ -33,9 +33,20 @@ class BenchmarkCase: # --------------------------------------------------------------------------- BALIBASE_URL = "http://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz" +BALIBASE_URL_FALLBACK = "https://web.archive.org/web/20231117003408/https://www.lbgi.fr/balibase/BalibaseDownload/BAliBASE_R1-5.tar.gz" BALIBASE_DIR = DOWNLOADS_DIR / "bb3_release" +def _validate_tarball(path: Path) -> bool: + """Check that a file is a valid gzip tarball (not an HTML error page).""" + try: + with tarfile.open(path) as tf: + tf.getnames() + return True + except (tarfile.ReadError, tarfile.CompressionError, EOFError): + return False + + def balibase_download() -> None: """Download and extract BAliBASE R1-5.""" DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True) @@ -44,12 +55,37 @@ def balibase_download() -> None: if BALIBASE_DIR.exists(): return - print(f"Downloading BAliBASE from {BALIBASE_URL} ...") - urllib.request.urlretrieve(BALIBASE_URL, tarball) + if tarball.exists() and not _validate_tarball(tarball): + print(f" Removing corrupt tarball: {tarball}") + tarball.unlink() + + if not tarball.exists(): + for url in [BALIBASE_URL, BALIBASE_URL_FALLBACK]: + try: + print(f"Downloading BAliBASE from {url} ...") + urllib.request.urlretrieve(url, tarball) + if not _validate_tarball(tarball): + print(f" Downloaded file is not a valid tarball (got HTML error page?)") + tarball.unlink() + continue + break + except (urllib.error.HTTPError, urllib.error.URLError) as e: + print(f" Failed: {e}") + if tarball.exists(): + tarball.unlink() + continue + else: + raise RuntimeError( + "Could not download BAliBASE from any URL.\n" + "Please download manually and place at:\n" + f" {tarball}\n" + "Or extract bb3_release/ into:\n" + f" {DOWNLOADS_DIR}/" + ) + print("Extracting ...") with tarfile.open(tarball) as tf: tf.extractall(DOWNLOADS_DIR) - tarball.unlink() def balibase_is_available() -> bool: @@ -107,8 +143,22 @@ def bralibase_download() -> None: if target.exists(): continue tarball = DOWNLOADS_DIR / f"{name}.tar.gz" - print(f"Downloading BRAliBASE {name} from {url} ...") - urllib.request.urlretrieve(url, tarball) + if tarball.exists() and not _validate_tarball(tarball): + print(f" Removing corrupt tarball: {tarball}") + tarball.unlink() + if not tarball.exists(): + print(f"Downloading BRAliBASE {name} from {url} ...") + try: + urllib.request.urlretrieve(url, tarball) + except (urllib.error.HTTPError, urllib.error.URLError) as e: + raise RuntimeError(f"Could not download BRAliBASE {name}: {e}") + if not _validate_tarball(tarball): + tarball.unlink() + raise RuntimeError( + f"Downloaded BRAliBASE {name} is not a valid tarball.\n" + f"Please download manually from:\n {url}\n" + f"and place at:\n {tarball}" + ) print("Extracting ...") with tarfile.open(tarball) as tf: tf.extractall(DOWNLOADS_DIR) @@ -239,6 +289,140 @@ def balifam_cases() -> List[BenchmarkCase]: return cases +# --------------------------------------------------------------------------- +# MDSA (Multiple DNA Sequence Alignments — Carroll et al. 2007) +# --------------------------------------------------------------------------- + +MDSA_BASE_URL = "https://dna.cs.byu.edu/mdsas/tarred" +MDSA_DIR = DOWNLOADS_DIR / "mdsa" +# Skip PREFAB (all pairwise, 2 seqs — not useful for MSA benchmarking) +MDSA_DATABASES = ["balibase", "oxbench", "smart"] +MDSA_VERSION = "all" # "100s" is too small (400 cases); "all" has 1,869 usable cases +MDSA_MAX_SEQS = 500 # Skip very large families (SMART has up to 1,769 seqs) + + +def mdsa_download() -> None: + """Download and extract MDSA DNA alignment benchmark datasets.""" + MDSA_DIR.mkdir(parents=True, exist_ok=True) + + for db in MDSA_DATABASES: + db_dir = MDSA_DIR / f"{db}_mdsa_{MDSA_VERSION}" + if db_dir.exists() and any(db_dir.glob("*.afa")): + continue + + tarball = MDSA_DIR / f"{db}_mdsa_{MDSA_VERSION}.tar.gz" + if tarball.exists() and not _validate_tarball(tarball): + print(f" Removing corrupt tarball: {tarball}") + tarball.unlink() + if not tarball.exists(): + url = f"{MDSA_BASE_URL}/{db}_mdsa_{MDSA_VERSION}.tar.gz" + print(f"Downloading MDSA {db} from {url} ...") + urllib.request.urlretrieve(url, tarball) + if not _validate_tarball(tarball): + tarball.unlink() + raise RuntimeError( + f"Downloaded MDSA {db} is not a valid tarball.\n" + f"Please download manually from:\n {url}\n" + f"and place at:\n {tarball}" + ) + + print(f"Extracting {db} ...") + with tarfile.open(tarball) as tf: + tf.extractall(MDSA_DIR) + + # Generate unaligned files by stripping gaps from reference .afa files + unaligned_dir = MDSA_DIR / "unaligned" + unaligned_dir.mkdir(exist_ok=True) + + for db in MDSA_DATABASES: + db_dir = MDSA_DIR / f"{db}_mdsa_{MDSA_VERSION}" + if not db_dir.exists(): + continue + db_unaligned = unaligned_dir / db + db_unaligned.mkdir(exist_ok=True) + for afa in sorted(db_dir.glob("*.afa")): + out = db_unaligned / afa.name + if out.exists(): + continue + _strip_gaps_fasta(afa, out) + + print(f"MDSA ready: {sum(1 for _ in unaligned_dir.rglob('*.afa'))} unaligned files") + + +def _strip_gaps_fasta(ref_path: Path, out_path: Path) -> None: + """Read aligned FASTA, strip gap characters, write unaligned FASTA.""" + sequences = [] + current_header = None + current_seq = [] + + with open(ref_path) as f: + for line in f: + line = line.rstrip("\n") + if line.startswith(">"): + if current_header is not None: + seq = "".join(current_seq).replace("-", "").replace(".", "") + sequences.append((current_header, seq)) + current_header = line + current_seq = [] + else: + current_seq.append(line) + if current_header is not None: + seq = "".join(current_seq).replace("-", "").replace(".", "") + sequences.append((current_header, seq)) + + with open(out_path, "w") as f: + for header, seq in sequences: + f.write(f"{header}\n{seq}\n") + + +def mdsa_is_available() -> bool: + unaligned_dir = MDSA_DIR / "unaligned" + return unaligned_dir.exists() and any(unaligned_dir.rglob("*.afa")) + + +def mdsa_cases() -> List[BenchmarkCase]: + """Discover MDSA test cases. + + Reference = original .afa (aligned FASTA) + Unaligned = gap-stripped version in unaligned/{db}/*.afa + Skips PREFAB (pairwise) and very large families (>MDSA_MAX_SEQS). + """ + cases = [] + unaligned_dir = MDSA_DIR / "unaligned" + + for db in MDSA_DATABASES: + ref_dir = MDSA_DIR / f"{db}_mdsa_{MDSA_VERSION}" + ua_dir = unaligned_dir / db + + if not ref_dir.exists() or not ua_dir.exists(): + continue + + for ref in sorted(ref_dir.glob("*.afa")): + unaligned = ua_dir / ref.name + if not unaligned.exists(): + continue + + # Count sequences and skip very large families + n_seqs = sum(1 for line in open(ref) if line.startswith(">")) + if n_seqs > MDSA_MAX_SEQS: + continue + # Skip trivial pairwise cases + if n_seqs < 3: + continue + + cases.append( + BenchmarkCase( + family=ref.stem, + dataset=f"mdsa_{db}", + unaligned=unaligned, + reference=ref, + seq_type="dna", + ) + ) + + return cases + + # --------------------------------------------------------------------------- # Registry # --------------------------------------------------------------------------- @@ -259,6 +443,11 @@ def balifam_cases() -> List[BenchmarkCase]: "is_available": balifam_is_available, "cases": balifam_cases, }, + "mdsa": { + "download": mdsa_download, + "is_available": mdsa_is_available, + "cases": mdsa_cases, + }, } diff --git a/benchmarks/optimize_ensemble.py b/benchmarks/optimize_ensemble.py new file mode 100644 index 0000000..dc24d24 --- /dev/null +++ b/benchmarks/optimize_ensemble.py @@ -0,0 +1,1102 @@ +#!/usr/bin/env python3 +"""Multi-objective ensemble hyperparameter optimization for kalign using pymoo. + +Optimizes per-run parameters for kalign's ensemble alignment mode, where N +independent alignments (each with different gap penalties, matrices, and tree +noise) are combined via POAR consensus. + +Uses stratified k-fold cross-validation so NSGA-II optimises on held-out +scores, not training scores. + +Objectives (maximized): + 1. Mean held-out F1 across folds (category-averaged within each fold) + 2. Mean held-out TC across folds (category-averaged within each fold) + +Per-run decision variables (× N runs): + - gpo: gap open penalty [2.0, 15.0] + - gpe: gap extend penalty [0.5, 5.0] + - tgpe: terminal gap extend [0.1, 3.0] + - noise: tree perturbation sigma [0.0, 0.5] + - matrix: substitution matrix {PFASUM43, PFASUM60, CorBLOSUM66} + +Shared decision variables: + - vsm_amax: variable scoring matrix [0.0, 5.0] + - consistency: anchor consistency rounds {0, 1, 2, 3, 5, 8} + - consistency_weight: consistency bonus weight [0.5, 5.0] + - realign: tree-rebuild iterations {0, 1, 2} + - min_support: POAR consensus threshold {0, 1, ..., N} + +Usage: + # Quick test + uv run python -m benchmarks.optimize_ensemble --n-runs 3 --pop-size 20 --n-gen 5 + + # Production (3 runs, Threadripper) + uv run python -m benchmarks.optimize_ensemble \\ + --n-runs 3 --pop-size 100 --n-gen 50 --n-workers 56 + + # Production (5 runs) + uv run python -m benchmarks.optimize_ensemble \\ + --n-runs 5 --pop-size 150 --n-gen 60 --n-workers 56 + + # Production (8 runs) + uv run python -m benchmarks.optimize_ensemble \\ + --n-runs 8 --pop-size 200 --n-gen 80 --n-workers 56 + + # Resume after interrupt + uv run python -m benchmarks.optimize_ensemble \\ + --resume benchmarks/results/ensemble_optim/gen_checkpoint.pkl --n-gen 80 +""" + +import argparse +import os +import pickle +import signal +import sys +import tempfile +import time +from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import numpy as np + +try: + from pymoo.algorithms.moo.nsga2 import NSGA2 # type: ignore[import-untyped] + from pymoo.core.callback import Callback # type: ignore[import-untyped] + from pymoo.core.problem import Problem # type: ignore[import-untyped] + from pymoo.operators.crossover.sbx import SBX # type: ignore[import-untyped] + from pymoo.operators.mutation.pm import PM # type: ignore[import-untyped] + from pymoo.operators.sampling.lhs import LHS # type: ignore[import-untyped] + from pymoo.optimize import minimize # type: ignore[import-untyped] + from pymoo.termination import get_termination # type: ignore[import-untyped] +except ImportError: + print("pymoo not installed. Run: uv pip install pymoo") + sys.exit(1) + +from rich.console import Console # type: ignore[import-untyped] +from rich.layout import Layout # type: ignore[import-untyped] +from rich.live import Live # type: ignore[import-untyped] +from rich.panel import Panel # type: ignore[import-untyped] +from rich.progress import ( # type: ignore[import-untyped] + BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, +) +from rich.table import Table # type: ignore[import-untyped] +from rich.text import Text # type: ignore[import-untyped] + +from kalign._core import ( # type: ignore[import-untyped] + PROTEIN, PROTEIN_PFASUM43, PROTEIN_PFASUM60, REFINE_CONFIDENT, + ensemble_custom_file_to_file, +) +import kalign # type: ignore[import-untyped] + +from .datasets import BenchmarkCase, balibase_cases, balibase_download, balibase_is_available +from .scoring import score_alignment_detailed + +# --------------------------------------------------------------------------- +# Parameter space definition +# --------------------------------------------------------------------------- + +# Per-run continuous: [gpo, gpe, tgpe, noise] × N_RUNS +PER_RUN_CONT_LOWER = np.array([2.0, 0.5, 0.1, 0.0]) +PER_RUN_CONT_UPPER = np.array([15.0, 5.0, 3.0, 0.5]) +PER_RUN_CONT_NAMES = ["gpo", "gpe", "tgpe", "noise"] +N_PER_RUN_CONT = len(PER_RUN_CONT_LOWER) + +# Per-run integer: [matrix] × N_RUNS +# matrix: 0=PFASUM43, 1=PFASUM60, 2=CorBLOSUM66 +N_PER_RUN_INT = 1 # just matrix + +# Shared continuous: [vsm_amax, consistency_weight] +SHARED_CONT_LOWER = np.array([0.0, 0.5]) +SHARED_CONT_UPPER = np.array([5.0, 5.0]) +SHARED_CONT_NAMES = ["vsm_amax", "consistency_weight"] +N_SHARED_CONT = len(SHARED_CONT_LOWER) + +# Shared integer: [consistency, realign, min_support] +# consistency: 0-5 maps via CONSISTENCY_MAP +# realign: 0-2 +# min_support: 0-N_RUNS (0=auto) +N_SHARED_INT = 3 # consistency, realign, min_support + +CONSISTENCY_MAP = [0, 1, 2, 3, 5, 8] +MATRIX_MAP = [PROTEIN_PFASUM43, PROTEIN_PFASUM60, PROTEIN] +MATRIX_NAMES = {PROTEIN_PFASUM43: "P43", PROTEIN_PFASUM60: "P60", PROTEIN: "CB66"} + + +def get_var_counts(n_runs: int): + """Return (n_cont, n_int, n_var) for a given number of ensemble runs.""" + n_cont = n_runs * N_PER_RUN_CONT + N_SHARED_CONT + n_int = n_runs * N_PER_RUN_INT + N_SHARED_INT + n_var = n_cont + n_int + return n_cont, n_int, n_var + + +def get_bounds(n_runs: int): + """Return (xl, xu) arrays for the full decision vector.""" + # Continuous: [per_run_cont × N_RUNS, shared_cont] + cont_lower = np.concatenate([np.tile(PER_RUN_CONT_LOWER, n_runs), SHARED_CONT_LOWER]) + cont_upper = np.concatenate([np.tile(PER_RUN_CONT_UPPER, n_runs), SHARED_CONT_UPPER]) + + # Integer: [matrix × N_RUNS, consistency, realign, min_support] + int_lower = np.zeros(n_runs * N_PER_RUN_INT + N_SHARED_INT) + int_upper = np.concatenate([ + np.full(n_runs, 2.0), # matrix: 0-2 + [len(CONSISTENCY_MAP) - 1], # consistency: 0-5 + [2.0], # realign: 0-2 + [float(n_runs)], # min_support: 0-N + ]) + + xl = np.concatenate([cont_lower, int_lower]) + xu = np.concatenate([cont_upper, int_upper]) + return xl, xu + + +def decode_ensemble_params(x, n_runs: int): + """Decode a decision vector into ensemble parameter dict. + + Returns dict with: + run_gpo, run_gpe, run_tgpe, run_noise: lists of float (length n_runs) + run_types: list of int (length n_runs) + vsm_amax, consistency_weight: float + consistency, realign, min_support: int + """ + n_cont, n_int, _ = get_var_counts(n_runs) + cont = x[:n_cont] + ints = np.round(x[n_cont:]).astype(int) + + # Per-run continuous + run_gpo = [] + run_gpe = [] + run_tgpe = [] + run_noise = [] + for k in range(n_runs): + offset = k * N_PER_RUN_CONT + run_gpo.append(float(cont[offset + 0])) + run_gpe.append(float(cont[offset + 1])) + run_tgpe.append(float(cont[offset + 2])) + run_noise.append(float(cont[offset + 3])) + + # Shared continuous + shared_offset = n_runs * N_PER_RUN_CONT + vsm_amax = float(cont[shared_offset + 0]) + consistency_weight = float(cont[shared_offset + 1]) + + # Per-run integer (matrix) + run_types = [] + for k in range(n_runs): + matrix_idx = int(np.clip(ints[k], 0, len(MATRIX_MAP) - 1)) + run_types.append(MATRIX_MAP[matrix_idx]) + + # Shared integer + shared_int_offset = n_runs * N_PER_RUN_INT + consistency_idx = int(np.clip(ints[shared_int_offset + 0], 0, len(CONSISTENCY_MAP) - 1)) + realign = int(np.clip(ints[shared_int_offset + 1], 0, 2)) + min_support = int(np.clip(ints[shared_int_offset + 2], 0, n_runs)) + + return { + "run_gpo": run_gpo, + "run_gpe": run_gpe, + "run_tgpe": run_tgpe, + "run_noise": run_noise, + "run_types": run_types, + "vsm_amax": vsm_amax, + "consistency_weight": consistency_weight, + "consistency": CONSISTENCY_MAP[consistency_idx], + "realign": realign, + "min_support": min_support, + } + + +def format_run_short(params, k): + """Short string for one run's params.""" + mat = MATRIX_NAMES.get(params["run_types"][k], "?") + return (f"gpo={params['run_gpo'][k]:.1f} gpe={params['run_gpe'][k]:.2f} " + f"tgpe={params['run_tgpe'][k]:.2f} n={params['run_noise'][k]:.2f} {mat}") + + +def format_ensemble_short(params): + """Compact summary of ensemble params.""" + n_runs = len(params["run_gpo"]) + runs = [] + for k in range(n_runs): + mat = MATRIX_NAMES.get(params["run_types"][k], "?") + runs.append(f"R{k}:{params['run_gpo'][k]:.1f}/{mat}") + run_str = " ".join(runs) + shared_str = (f"vsm={params['vsm_amax']:.1f} c={params['consistency']} " + f"cw={params['consistency_weight']:.1f} re={params['realign']} " + f"ms={params['min_support']}") + return f"{run_str} | {shared_str}" + + +def format_ensemble_long(params): + """Verbose multi-line summary.""" + lines = [] + n_runs = len(params["run_gpo"]) + for k in range(n_runs): + lines.append(f" Run {k}: {format_run_short(params, k)}") + lines.append(f" Shared: vsm={params['vsm_amax']:.2f} " + f"cons={params['consistency']} cw={params['consistency_weight']:.2f} " + f"re={params['realign']} ms={params['min_support']}") + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Stratified k-fold CV (reused from optimize_params) +# --------------------------------------------------------------------------- + +def stratified_kfold(cases: List[BenchmarkCase], k: int, seed: int = 42 + ) -> List[Tuple[List[BenchmarkCase], List[BenchmarkCase]]]: + """Split cases into k stratified folds by dataset (RV category).""" + rng = np.random.RandomState(seed) + + by_cat: Dict[str, List[BenchmarkCase]] = defaultdict(list) + for c in cases: + by_cat[c.dataset].append(c) + + fold_assignments: Dict[str, int] = {} + for cat_cases in by_cat.values(): + indices = list(range(len(cat_cases))) + rng.shuffle(indices) + for rank, idx in enumerate(indices): + fold_assignments[cat_cases[idx].family] = rank % k + + folds = [] + for fold_idx in range(k): + test = [c for c in cases if fold_assignments[c.family] == fold_idx] + train = [c for c in cases if fold_assignments[c.family] != fold_idx] + folds.append((train, test)) + + return folds + + +# --------------------------------------------------------------------------- +# Evaluation +# --------------------------------------------------------------------------- + +def evaluate_ensemble(params, cases, n_threads=1, quiet=True): + """Run ensemble alignment with given params on all cases, return mean metrics.""" + results_by_cat: Dict[str, list] = {} + total_time = 0.0 + + for case in cases: + with tempfile.TemporaryDirectory() as tmpdir: + output = Path(tmpdir) / f"{case.family}_aln.fasta" + + try: + start = time.perf_counter() + ensemble_custom_file_to_file( + str(case.unaligned), + str(output), + run_gpo=params["run_gpo"], + run_gpe=params["run_gpe"], + run_tgpe=params["run_tgpe"], + run_noise=params["run_noise"], + run_types=params["run_types"], + format="fasta", + seq_type=PROTEIN, + seed=42, + min_support=params["min_support"], + refine=REFINE_CONFIDENT, + vsm_amax=params["vsm_amax"], + realign=params["realign"], + seq_weights=-1.0, + n_threads=n_threads, + consistency_anchors=params["consistency"], + consistency_weight=params["consistency_weight"], + ) + wall_time = time.perf_counter() - start + total_time += wall_time + + detailed = score_alignment_detailed(case.reference, output) + + cat = case.dataset + if cat not in results_by_cat: + results_by_cat[cat] = [] + results_by_cat[cat].append(detailed) + + except Exception as e: + if not quiet: + print(f" WARN: {case.family}: {e}", file=sys.stderr) + + if not results_by_cat: + return {"f1": 0.0, "tc": 0.0, "recall": 0.0, "precision": 0.0, + "wall_time": total_time, "per_category": {}} + + per_cat = {} + for cat, scores in results_by_cat.items(): + per_cat[cat] = { + "f1": np.mean([s["f1"] for s in scores]), + "tc": np.mean([s["tc"] for s in scores]), + "recall": np.mean([s["recall"] for s in scores]), + "precision": np.mean([s["precision"] for s in scores]), + "n": len(scores), + } + + all_f1 = [v["f1"] for v in per_cat.values()] + all_tc = [v["tc"] for v in per_cat.values()] + all_recall = [v["recall"] for v in per_cat.values()] + all_precision = [v["precision"] for v in per_cat.values()] + + return { + "f1": float(np.mean(all_f1)), + "tc": float(np.mean(all_tc)), + "recall": float(np.mean(all_recall)), + "precision": float(np.mean(all_precision)), + "wall_time": total_time, + "per_category": per_cat, + } + + +def evaluate_ensemble_cv(params, folds, n_threads=1, quiet=True): + """Evaluate ensemble params using stratified k-fold CV.""" + fold_f1s = [] + fold_tcs = [] + total_time = 0.0 + + for _, test in folds: + result = evaluate_ensemble(params, test, n_threads, quiet) + fold_f1s.append(result["f1"]) + fold_tcs.append(result["tc"]) + total_time += result["wall_time"] + + return { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, + "fold_tcs": fold_tcs, + "wall_time": total_time, + } + + +# --------------------------------------------------------------------------- +# Rich live dashboard +# --------------------------------------------------------------------------- + +class Dashboard: + """Rich-based live terminal dashboard for ensemble optimization progress.""" + + def __init__(self, n_gen: int, pop_size: int, baseline_f1: float, baseline_tc: float): + self.n_gen = n_gen + self.pop_size = pop_size + self.baseline_f1 = baseline_f1 + self.baseline_tc = baseline_tc + self.console = Console() + + self.current_gen = 0 + self.eval_count = 0 + self.total_evals = n_gen * pop_size + self.gen_start_time = time.time() + self.run_start_time = time.time() + self.current_eval_params: Optional[dict] = None + self.current_eval_idx = 0 + + self.best_f1 = 0.0 + self.best_f1_params: Optional[dict] = None + self.best_tc = 0.0 + self.best_tc_params: Optional[dict] = None + + self.pareto_front: List[dict] = [] + self.gen_history: List[dict] = [] + self.recent_evals: List[dict] = [] + + self.progress = Progress( + SpinnerColumn(), + TextColumn("[bold blue]{task.description}"), + BarColumn(bar_width=40), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TimeElapsedColumn(), + ) + self.gen_task = self.progress.add_task("Generation", total=n_gen) + self.eval_task = self.progress.add_task(" Eval", total=pop_size) + + self.live = Live(self._build_layout(), console=self.console, refresh_per_second=2) + + def start(self): + self.run_start_time = time.time() + self.live.start() + + def stop(self): + self.live.stop() + + def _delta_str(self, value: float, baseline: float) -> str: + delta = value - baseline + if delta > 0.005: + return f"[bold green]{value:.4f} (+{delta:.4f})[/]" + elif delta < -0.005: + return f"[bold red]{value:.4f} ({delta:.4f})[/]" + else: + return f"[dim]{value:.4f} ({delta:+.4f})[/]" + + def _build_status_panel(self) -> Panel: + elapsed = time.time() - self.run_start_time + gen_elapsed = time.time() - self.gen_start_time + + if self.eval_count > 0: + avg_per_eval = elapsed / self.eval_count + remaining = avg_per_eval * (self.total_evals - self.eval_count) + eta_str = f"{remaining / 60:.0f}m" if remaining > 60 else f"{remaining:.0f}s" + else: + eta_str = "..." + + lines = [ + f"Gen [bold]{self.current_gen}[/]/{self.n_gen} " + f"Eval [bold]{self.eval_count}[/]/{self.total_evals} " + f"Elapsed [bold]{elapsed / 60:.1f}m[/] " + f"ETA [bold]{eta_str}[/] " + f"Gen time [bold]{gen_elapsed:.1f}s[/]", + ] + + if self.current_eval_params: + lines.append(f"[dim]Current: {format_ensemble_short(self.current_eval_params)}[/]") + + return Panel("\n".join(lines), title="Ensemble Optimization", border_style="blue") + + def _build_best_panel(self) -> Panel: + lines = [] + lines.append(f"[dim]Baseline: F1={self.baseline_f1:.4f} TC={self.baseline_tc:.4f}[/]") + lines.append("") + + if self.best_f1_params: + lines.append(f"Best F1: {self._delta_str(self.best_f1, self.baseline_f1)}") + lines.append(f" [dim]{format_ensemble_short(self.best_f1_params)}[/]") + else: + lines.append("[dim]Best F1: (no evaluations yet)[/]") + + lines.append("") + + if self.best_tc_params: + lines.append(f"Best TC: {self._delta_str(self.best_tc, self.baseline_tc)}") + lines.append(f" [dim]{format_ensemble_short(self.best_tc_params)}[/]") + else: + lines.append("[dim]Best TC: (no evaluations yet)[/]") + + return Panel("\n".join(lines), title="Best Solutions", border_style="green") + + def _build_pareto_table(self) -> Panel: + table = Table(show_header=True, expand=True, padding=(0, 1)) + table.add_column("#", style="dim", width=3) + table.add_column("CV F1", justify="right", width=8) + table.add_column("CV TC", justify="right", width=8) + table.add_column("Parameters") + + for i, cfg in enumerate(self.pareto_front[:8]): + f1_style = "bold green" if cfg["f1"] > self.baseline_f1 else "" + tc_style = "bold green" if cfg["tc"] > self.baseline_tc else "" + table.add_row( + str(i), + Text(f"{cfg['f1']:.4f}", style=f1_style), + Text(f"{cfg['tc']:.4f}", style=tc_style), + format_ensemble_short(cfg["params"]), + ) + + return Panel(table, title="Pareto Front", border_style="yellow") + + def _build_trend_panel(self) -> Panel: + if not self.gen_history: + return Panel("[dim]No data yet[/]", title="Trend", border_style="cyan") + + entries = [] + step = max(1, len(self.gen_history) // 8) + for i in range(0, len(self.gen_history), step): + h = self.gen_history[i] + entries.append(f"Gen {h['gen']:>3}: F1={h['best_f1']:.4f}") + if self.gen_history[-1] not in [self.gen_history[i] for i in range(0, len(self.gen_history), step)]: + h = self.gen_history[-1] + entries.append(f"Gen {h['gen']:>3}: F1={h['best_f1']:.4f}") + + return Panel(" ".join(entries), title="Trend", border_style="cyan") + + def _build_recent_panel(self) -> Panel: + if not self.recent_evals: + return Panel("[dim]No evaluations yet[/]", title="Recent", border_style="magenta") + + lines = [] + for ev in self.recent_evals[-5:]: + lines.append(f"F1={ev['f1']:.4f} TC={ev['tc']:.4f} {format_ensemble_short(ev['params'])}") + + return Panel("\n".join(lines), title="Recent", border_style="magenta") + + def _build_layout(self) -> Layout: + layout = Layout() + layout.split_column( + Layout(self._build_status_panel(), name="status", size=4), + Layout(self.progress, name="progress", size=3), + Layout(self._build_best_panel(), name="best", size=9), + Layout(self._build_pareto_table(), name="pareto", size=12), + Layout(self._build_trend_panel(), name="trend", size=3), + Layout(self._build_recent_panel(), name="recent", size=8), + ) + return layout + + def _refresh(self): + self.live.update(self._build_layout()) + + def on_eval_start(self, params, eval_count, idx_in_gen): + self.current_eval_params = params + self.eval_count = eval_count + self.current_eval_idx = idx_in_gen + self.progress.update(self.eval_task, completed=idx_in_gen) + self._refresh() + + def on_eval_end(self, params, cv_result): + f1 = cv_result["f1"] + tc = cv_result["tc"] + + if f1 > self.best_f1: + self.best_f1 = f1 + self.best_f1_params = params + if tc > self.best_tc: + self.best_tc = tc + self.best_tc_params = params + + self.recent_evals.append({"params": params, "f1": f1, "tc": tc}) + if len(self.recent_evals) > 10: + self.recent_evals = self.recent_evals[-10:] + + self._refresh() + + def on_gen_start(self, gen): + self.current_gen = gen + self.gen_start_time = time.time() + self.progress.update(self.gen_task, completed=gen - 1) + self.progress.update(self.eval_task, completed=0) + self._refresh() + + def on_gen_end(self, gen, pareto): + self.current_gen = gen + self.progress.update(self.gen_task, completed=gen) + self.progress.update(self.eval_task, completed=self.pop_size) + + self.pareto_front = sorted(pareto, key=lambda x: -x["f1"]) + + best_f1_in_gen = max(p["f1"] for p in pareto) if pareto else 0.0 + self.gen_history.append({"gen": gen, "best_f1": best_f1_in_gen}) + + self._refresh() + + +# --------------------------------------------------------------------------- +# ProcessPoolExecutor helper +# --------------------------------------------------------------------------- + +def _kill_pool(pool: ProcessPoolExecutor) -> None: + """Forcefully terminate all worker processes in the pool.""" + for pid in list(pool._processes): # noqa: SLF001 + try: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + pool.shutdown(wait=False, cancel_futures=True) + + +# Top-level function for pickling (ProcessPoolExecutor requires this) +def _eval_one_ensemble(args_tuple): + """Evaluate one ensemble parameter vector. Top-level for ProcessPoolExecutor.""" + x, n_runs, folds, n_threads = args_tuple + params = decode_ensemble_params(x, n_runs) + cv_result = evaluate_ensemble_cv(params, folds, n_threads, quiet=True) + return params, cv_result + + +def _eval_one_ensemble_fold(args_tuple): + """Evaluate one (individual, fold) pair for ensemble. Fine-grained parallelism.""" + ind_idx, fold_idx, x, n_runs, test_cases, n_threads = args_tuple + params = decode_ensemble_params(x, n_runs) + result = evaluate_ensemble(params, test_cases, n_threads, quiet=True) + return ind_idx, fold_idx, params, result + + +# --------------------------------------------------------------------------- +# pymoo Problem + Callback +# --------------------------------------------------------------------------- + +class EnsembleCVProblem(Problem): + """Multi-objective optimization for ensemble with stratified CV.""" + + def __init__(self, n_runs, folds, n_threads=1, n_workers=1, + optimize_time=False, dashboard: Optional[Dashboard] = None): + xl, xu = get_bounds(n_runs) + n_cont, n_int, n_var = get_var_counts(n_runs) + n_obj = 3 if optimize_time else 2 + + super().__init__( + n_var=n_var, + n_obj=n_obj, + xl=xl, + xu=xu, + ) + self.n_runs = n_runs + self.folds = folds + self.n_threads = n_threads + self.n_workers = n_workers + self.optimize_time = optimize_time + self.dashboard = dashboard + self.eval_count = 0 + self.history: List[dict] = [] + + def _evaluate(self, X, out, *args, **kwargs): # pyright: ignore[reportUnusedVariable] + F = np.zeros((X.shape[0], self.n_obj)) + + if self.n_workers > 1: + self._evaluate_parallel(X, F) + else: + self._evaluate_serial(X, F) + + out["F"] = F + + def _evaluate_serial(self, X, F): + for i, x in enumerate(X): + params = decode_ensemble_params(x, self.n_runs) + self.eval_count += 1 + + if self.dashboard: + self.dashboard.on_eval_start(params, self.eval_count, i) + + cv_result = evaluate_ensemble_cv(params, self.folds, self.n_threads, quiet=True) + self._record(i, F, params, cv_result) + + def _evaluate_parallel(self, X, F): + """Fine-grained parallelism: submit (individual × fold) jobs.""" + n_folds = len(self.folds) + + jobs = [] + for i, x in enumerate(X): + for fold_idx, (_, test) in enumerate(self.folds): + jobs.append((i, fold_idx, x, self.n_runs, test, self.n_threads)) + + if self.dashboard: + self.dashboard.on_eval_start( + decode_ensemble_params(X[0], self.n_runs), + self.eval_count + 1, 0) + + fold_results: Dict[int, Dict[int, dict]] = defaultdict(dict) + ind_params: Dict[int, dict] = {} + + pool = ProcessPoolExecutor(max_workers=self.n_workers) + try: + futures = {pool.submit(_eval_one_ensemble_fold, j): j[:2] for j in jobs} + completed_individuals = set() + + for future in as_completed(futures): + ind_idx, fold_idx, params, result = future.result() + fold_results[ind_idx][fold_idx] = result + ind_params[ind_idx] = params + + if len(fold_results[ind_idx]) == n_folds: + completed_individuals.add(ind_idx) + self.eval_count += 1 + + fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] + fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] + total_time = sum(fold_results[ind_idx][fi]["wall_time"] for fi in range(n_folds)) + + cv_result = { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, + "fold_tcs": fold_tcs, + "wall_time": total_time, + } + + self._record(ind_idx, F, params, cv_result) + + if self.dashboard: + self.dashboard.on_eval_start( + params, self.eval_count, len(completed_individuals)) + + except KeyboardInterrupt: + _kill_pool(pool) + raise + finally: + pool.shutdown(wait=False) + + def _record(self, i, F, params, cv_result): + F[i, 0] = -cv_result["f1"] + F[i, 1] = -cv_result["tc"] + if self.optimize_time: + F[i, 2] = cv_result["wall_time"] + + self.history.append({ + "eval": self.eval_count, + "params": params, + "cv_result": cv_result, + }) + + if self.dashboard: + self.dashboard.on_eval_end(params, cv_result) + + +class GenerationCallback(Callback): + """pymoo callback: updates dashboard + saves checkpoint after each generation.""" + + def __init__(self, dashboard: Optional[Dashboard] = None, + checkpoint_path: Optional[Path] = None, + problem: Optional[EnsembleCVProblem] = None): + super().__init__() + self.dashboard = dashboard + self.checkpoint_path = checkpoint_path + self.problem = problem + + def notify(self, algorithm): + gen = algorithm.n_gen + + if self.dashboard: + self.dashboard.on_gen_start(gen) + + pareto = [] + if algorithm.opt is not None and len(algorithm.opt) > 0: + n_runs = self.problem.n_runs if self.problem else 3 + for ind in algorithm.opt: + params = decode_ensemble_params(ind.X, n_runs) + pareto.append({ + "params": params, + "f1": -ind.F[0], + "tc": -ind.F[1], + }) + + if self.dashboard: + self.dashboard.on_gen_end(gen, pareto) + + if self.checkpoint_path: + pop = algorithm.pop + ckpt = { + "pop_X": pop.get("X").copy(), + "pop_F": pop.get("F").copy(), + "n_gen_completed": gen, + "history": self.problem.history if self.problem else [], + "pop_size": len(pop), + "n_runs": self.problem.n_runs if self.problem else 3, + } + tmp = self.checkpoint_path.with_suffix(".tmp") + with open(tmp, "wb") as f: + pickle.dump(ckpt, f) + tmp.rename(self.checkpoint_path) + + +def load_checkpoint(path: Path): + """Load a generation checkpoint.""" + with open(path, "rb") as f: + ckpt = pickle.load(f) # noqa: S301 + return (ckpt["pop_X"], ckpt["pop_F"], ckpt["n_gen_completed"], + ckpt.get("history", []), ckpt.get("n_runs", 3)) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Optimize kalign ensemble hyperparameters with NSGA-II + stratified k-fold CV") + parser.add_argument("--n-runs", type=int, default=3, + help="Number of ensemble runs (default: 3)") + parser.add_argument("--pop-size", type=int, default=100, + help="Population size (default: 100)") + parser.add_argument("--n-gen", type=int, default=50, + help="Number of generations (default: 50)") + parser.add_argument("--n-folds", type=int, default=5, + help="Number of CV folds (default: 5)") + parser.add_argument("--n-threads", type=int, default=1, + help="OpenMP threads per alignment (default: 1)") + parser.add_argument("--n-workers", type=int, default=1, + help="Parallel worker processes (default: 1)") + parser.add_argument("--seed", type=int, default=42, + help="Random seed (default: 42)") + parser.add_argument("--optimize-time", action="store_true", + help="Add wall time as 3rd objective") + parser.add_argument("--output-dir", type=str, default="benchmarks/results/ensemble_optim", + help="Output directory") + parser.add_argument("--run-name", type=str, default=None, + help="Name for this run (creates subdirectory, e.g. 'run1_3runs')") + parser.add_argument("--no-dashboard", action="store_true", + help="Disable rich dashboard") + parser.add_argument("--resume", type=str, default=None, + help="Resume from a generation checkpoint file (.pkl)") + args = parser.parse_args() + + console = Console() + n_runs = args.n_runs + + # Dimensions + n_cont, n_int, n_var = get_var_counts(n_runs) + console.print(f"[bold]Ensemble optimizer[/]: {n_runs} runs, " + f"{n_var} decision variables ({n_cont} continuous + {n_int} integer)") + + # Setup + if not balibase_is_available(): + console.print("Downloading BAliBASE...") + balibase_download() + + cases = balibase_cases() + console.print(f"Loaded [bold]{len(cases)}[/] BAliBASE cases") + + cats: Dict[str, int] = {} + for c in cases: + cats[c.dataset] = cats.get(c.dataset, 0) + 1 + for cat, n in sorted(cats.items()): + console.print(f" {cat}: {n} cases") + + k = args.n_folds + folds = stratified_kfold(cases, k, seed=args.seed) + console.print(f"\nStratified [bold]{k}[/]-fold CV:") + for i, (train, test) in enumerate(folds): + test_cats: Dict[str, int] = defaultdict(int) + for c in test: + test_cats[c.dataset] += 1 + cat_str = ", ".join(f"{cat.replace('balibase_', '')}:{n}" + for cat, n in sorted(test_cats.items())) + console.print(f" Fold {i}: {len(test)} test / {len(train)} train ({cat_str})") + + output_dir = Path(args.output_dir) + if args.run_name: + output_dir = output_dir / args.run_name + output_dir.mkdir(parents=True, exist_ok=True) + + # --- Baseline evaluation (current best: ens3+vsm+ref+ra1) --- + console.print(f"\n[bold]Baseline evaluation[/] (ens{n_runs}+vsm+ref+ra1, {k}-fold CV)") + console.print("[dim]This uses the original kalign_ensemble with hardcoded scale factors[/]") + + # Evaluate baseline via the standard ensemble API + baseline_fold_f1s = [] + baseline_fold_tcs = [] + baseline_total_time = 0.0 + for fold_idx, (_, test) in enumerate(folds): + results_by_cat: Dict[str, list] = {} + for case in test: + with tempfile.TemporaryDirectory() as tmpdir: + output = Path(tmpdir) / f"{case.family}_aln.fasta" + try: + start = time.perf_counter() + kalign.align_file_to_file( + str(case.unaligned), str(output), + format="fasta", + ensemble=n_runs, + vsm_amax=2.0, + refine="confident", + realign=1, + ensemble_seed=42, + ) + baseline_total_time += time.perf_counter() - start + detailed = score_alignment_detailed(case.reference, output) + cat = case.dataset + if cat not in results_by_cat: + results_by_cat[cat] = [] + results_by_cat[cat].append(detailed) + except Exception as e: + console.print(f" [yellow]WARN[/]: {case.family}: {e}") + + if results_by_cat: + cat_f1s = [np.mean([s["f1"] for s in v]) for v in results_by_cat.values()] + cat_tcs = [np.mean([s["tc"] for s in v]) for v in results_by_cat.values()] + baseline_fold_f1s.append(float(np.mean(cat_f1s))) + baseline_fold_tcs.append(float(np.mean(cat_tcs))) + + baseline_cv = { + "f1": float(np.mean(baseline_fold_f1s)), + "tc": float(np.mean(baseline_fold_tcs)), + "f1_std": float(np.std(baseline_fold_f1s)), + "tc_std": float(np.std(baseline_fold_tcs)), + "fold_f1s": baseline_fold_f1s, + "fold_tcs": baseline_fold_tcs, + "wall_time": baseline_total_time, + } + console.print(f" CV F1=[bold]{baseline_cv['f1']:.4f}[/]±{baseline_cv['f1_std']:.4f} " + f"CV TC=[bold]{baseline_cv['tc']:.4f}[/]±{baseline_cv['tc_std']:.4f} " + f"time={baseline_cv['wall_time']:.1f}s") + for i, (f1, tc) in enumerate(zip(baseline_cv['fold_f1s'], baseline_cv['fold_tcs'])): + console.print(f" Fold {i}: F1={f1:.4f} TC={tc:.4f}") + + # --- Optimization --- + n_evals = args.pop_size * args.n_gen + est_sec_per_eval = baseline_cv["wall_time"] / k # baseline did k folds + parallelism = max(1, args.n_workers) + est_hours = n_evals * est_sec_per_eval / parallelism / 3600 + + console.print(f"\n[bold]Starting NSGA-II[/]: pop_size={args.pop_size}, n_gen={args.n_gen}, " + f"{k}-fold CV, {n_runs} ensemble runs, " + f"{args.n_workers} worker(s) × {args.n_threads} thread(s)") + console.print(f"Total evaluations: ~{n_evals}") + console.print(f"Estimated time: ~{est_hours:.1f} hours\n") + + use_dashboard = not args.no_dashboard + dashboard = None + + if use_dashboard: + dashboard = Dashboard( + n_gen=args.n_gen, + pop_size=args.pop_size, + baseline_f1=baseline_cv["f1"], + baseline_tc=baseline_cv["tc"], + ) + + problem = EnsembleCVProblem( + n_runs=n_runs, + folds=folds, + n_threads=args.n_threads, + n_workers=args.n_workers, + optimize_time=args.optimize_time, + dashboard=dashboard, + ) + + checkpoint_path = output_dir / "gen_checkpoint.pkl" + callback = GenerationCallback( + dashboard=dashboard, + checkpoint_path=checkpoint_path, + problem=problem, + ) + + # Resume from checkpoint or start fresh + resumed_gen = 0 + if args.resume: + resume_path = Path(args.resume) + if not resume_path.exists(): + console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") + return + pop_X, _pop_F, resumed_gen, prev_history, ckpt_n_runs = load_checkpoint(resume_path) + if ckpt_n_runs != n_runs: + console.print(f"[bold red]Checkpoint n_runs={ckpt_n_runs} != --n-runs={n_runs}[/]") + return + problem.history = prev_history + console.print(f"[bold green]Resumed[/] from generation {resumed_gen} " + f"({len(prev_history)} prior evaluations)") + remaining = args.n_gen - resumed_gen + if remaining <= 0: + console.print(f"[bold yellow]Already completed {resumed_gen} generations " + f"(requested {args.n_gen}). Increase --n-gen to continue.[/]") + return + termination = get_termination("n_gen", remaining) + algorithm = NSGA2( + pop_size=len(pop_X), + sampling=pop_X, + crossover=SBX(prob=0.9, eta=15), + mutation=PM(eta=20), + eliminate_duplicates=True, + ) + else: + algorithm = NSGA2( + pop_size=args.pop_size, + sampling=LHS(), + crossover=SBX(prob=0.9, eta=15), + mutation=PM(eta=20), + eliminate_duplicates=True, + ) + termination = get_termination("n_gen", args.n_gen) + + if dashboard: + dashboard.start() + + try: + res = minimize( + problem, + algorithm, + termination, + seed=args.seed, + verbose=not use_dashboard, + callback=callback, + ) + except KeyboardInterrupt: + if dashboard: + dashboard.stop() + console.print("\n[bold yellow]Interrupted![/] Checkpoint was saved after last " + f"completed generation to:\n {checkpoint_path}") + console.print("Resume with: [bold]--resume " + str(checkpoint_path) + "[/]") + os._exit(1) # skip atexit handlers that hang on worker join + finally: + if dashboard: + dashboard.stop() + + # --- Results --- + console.print(f"\n[bold]Optimization complete.[/] " + f"{len(res.F)} Pareto-optimal solutions found.\n") + + pareto_configs = [] + for i, (x, f) in enumerate(zip(res.X, res.F)): + params = decode_ensemble_params(x, n_runs) + f1 = -f[0] + tc = -f[1] + wt = f[2] if args.optimize_time else None + pareto_configs.append({"params": params, "f1_cv": f1, "tc_cv": tc, "wall_time": wt}) + + table = Table(title="Pareto Front (sorted by CV F1)") + table.add_column("#", style="dim", width=3) + table.add_column("CV F1", justify="right") + table.add_column("CV TC", justify="right") + table.add_column("Parameters") + + sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) + for i, cfg in enumerate(sorted_pareto): + f1_style = "bold green" if cfg["f1_cv"] > baseline_cv["f1"] else "" + tc_style = "bold green" if cfg["tc_cv"] > baseline_cv["tc"] else "" + table.add_row( + str(i), + Text(f"{cfg['f1_cv']:.4f}", style=f1_style), + Text(f"{cfg['tc_cv']:.4f}", style=tc_style), + format_ensemble_short(cfg["params"]), + ) + console.print(table) + + # --- Re-evaluate best on FULL dataset --- + best_f1_idx = np.argmin(res.F[:, 0]) + best = pareto_configs[best_f1_idx] + + console.print(f"\n{'='*60}") + console.print(f"[bold]Best CV F1 solution:[/] CV F1={best['f1_cv']:.4f} CV TC={best['tc_cv']:.4f}") + console.print(format_ensemble_long(best["params"])) + + console.print(f"\n[bold]Full-dataset evaluation[/] (checking for overfit)") + best_full = evaluate_ensemble(best["params"], cases, args.n_threads) + console.print(f" Full F1=[bold]{best_full['f1']:.4f}[/] Full TC=[bold]{best_full['tc']:.4f}[/] " + f"Recall={best_full['recall']:.4f} Precision={best_full['precision']:.4f}") + for cat, v in sorted(best_full["per_category"].items()): + console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") + + gap_f1 = best_full["f1"] - best["f1_cv"] + gap_tc = best_full["tc"] - best["tc_cv"] + console.print(f"\n Overfit check (full - CV): F1 {gap_f1:+.4f} TC {gap_tc:+.4f}") + if gap_f1 > 0.02: + console.print(" [bold yellow]Warning:[/] Full-data F1 notably higher than CV — possible overfit") + else: + console.print(" [bold green]OK:[/] Full-data and CV scores are consistent") + + # --- Save --- + checkpoint = { + "pareto_configs": pareto_configs, + "history": problem.history, + "baseline_cv": baseline_cv, + "best_full": best_full, + "folds_info": [(len(tr), len(te)) for tr, te in folds], + "args": vars(args), + } + ckpt_path = output_dir / "optim_checkpoint.pkl" + with open(ckpt_path, "wb") as f: + pickle.dump(checkpoint, f) + console.print(f"\nCheckpoint saved to {ckpt_path}") + + summary_path = output_dir / "pareto_front.txt" + with open(summary_path, "w") as f: + f.write(f"# Pareto-optimal kalign ensemble configs (NSGA-II + {k}-fold stratified CV)\n") + f.write(f"# n_runs={n_runs} pop_size={args.pop_size} n_gen={args.n_gen} " + f"n_cases={len(cases)} seed={args.seed}\n") + f.write(f"# Baseline CV: F1={baseline_cv['f1']:.4f} TC={baseline_cv['tc']:.4f}\n\n") + for i, cfg in enumerate(sorted_pareto): + p = cfg["params"] + f.write(f"[{i}] CV_F1={cfg['f1_cv']:.4f} CV_TC={cfg['tc_cv']:.4f}\n") + for rk in range(n_runs): + mat = MATRIX_NAMES.get(p["run_types"][rk], "?") + f.write(f" Run {rk}: gpo={p['run_gpo'][rk]:.3f} gpe={p['run_gpe'][rk]:.3f} " + f"tgpe={p['run_tgpe'][rk]:.3f} noise={p['run_noise'][rk]:.3f} " + f"matrix={mat}\n") + f.write(f" Shared: vsm_amax={p['vsm_amax']:.3f} " + f"consistency={p['consistency']} " + f"consistency_weight={p['consistency_weight']:.3f} " + f"realign={p['realign']} min_support={p['min_support']}\n\n") + console.print(f"Pareto front saved to {summary_path}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/optimize_params.py b/benchmarks/optimize_params.py new file mode 100644 index 0000000..9c273df --- /dev/null +++ b/benchmarks/optimize_params.py @@ -0,0 +1,1181 @@ +#!/usr/bin/env python3 +"""Multi-objective hyperparameter optimization for kalign using pymoo. + +Uses stratified k-fold cross-validation so NSGA-II optimises on held-out +scores, not training scores. Each evaluation splits BAliBASE families +into k folds (stratified by RV category), aligns k-1 folds, and scores +on the held-out fold. The objective is the mean held-out F1 / TC. + +Objectives (maximized): + 1. Mean held-out F1 across folds (category-averaged within each fold) + 2. Mean held-out TC across folds (category-averaged within each fold) + +Decision variables: + - gpo: gap open penalty [2.0, 15.0] + - gpe: gap extend penalty [0.5, 5.0] + - tgpe: terminal gap extend [0.1, 3.0] + - vsm_amax: variable scoring matrix [0.0, 5.0] + - seq_weights: profile rebalancing [0.0, 5.0] + - consistency: anchor consistency rounds {0, 1, 2, 3, 5, 8} + - consistency_weight: consistency bonus weight [0.5, 5.0] + - realign: tree-rebuild iterations {0, 1, 2} + - matrix: substitution matrix choice {PFASUM43, PFASUM60, CorBLOSUM66} + +Usage: + # Quick test (small pop, few generations) + uv run python -m benchmarks.optimize_params --pop-size 20 --n-gen 10 + + # Full optimization (5-fold CV, all 218 cases) + uv run python -m benchmarks.optimize_params --pop-size 60 --n-gen 80 --n-threads 4 + + # Faster: 3-fold CV + uv run python -m benchmarks.optimize_params --n-folds 3 --pop-size 40 --n-gen 50 + + # Add wall time as 3rd objective + uv run python -m benchmarks.optimize_params --optimize-time +""" + +import argparse +import os +import pickle +import signal +import sys +import tempfile +import time +from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import numpy as np + +try: + from pymoo.algorithms.moo.nsga2 import NSGA2 # type: ignore[import-untyped] + from pymoo.core.callback import Callback # type: ignore[import-untyped] + from pymoo.core.problem import Problem # type: ignore[import-untyped] + from pymoo.operators.crossover.sbx import SBX # type: ignore[import-untyped] + from pymoo.operators.mutation.pm import PM # type: ignore[import-untyped] + from pymoo.operators.sampling.lhs import LHS # type: ignore[import-untyped] + from pymoo.optimize import minimize # type: ignore[import-untyped] + from pymoo.termination import get_termination # type: ignore[import-untyped] +except ImportError: + print("pymoo not installed. Run: uv pip install pymoo") + sys.exit(1) + +from rich.console import Console # type: ignore[import-untyped] +from rich.layout import Layout # type: ignore[import-untyped] +from rich.live import Live # type: ignore[import-untyped] +from rich.panel import Panel # type: ignore[import-untyped] +from rich.progress import ( # type: ignore[import-untyped] + BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, +) +from rich.table import Table # type: ignore[import-untyped] +from rich.text import Text # type: ignore[import-untyped] + +import kalign # type: ignore[import-untyped] + +from .datasets import BenchmarkCase, balibase_cases, balibase_download, balibase_is_available +from .scoring import score_alignment_detailed + +# --------------------------------------------------------------------------- +# Parameter space definition +# --------------------------------------------------------------------------- + +# Continuous variables: [gpo, gpe, tgpe, vsm_amax, seq_weights, consistency_weight] +CONT_LOWER = np.array([2.0, 0.5, 0.1, 0.0, 0.0, 0.5]) +CONT_UPPER = np.array([15.0, 5.0, 3.0, 5.0, 5.0, 5.0]) +CONT_NAMES = ["gpo", "gpe", "tgpe", "vsm_amax", "seq_weights", "consistency_weight"] + +# Integer/categorical variables: [consistency, realign, matrix] +INT_LOWER = np.array([0, 0, 0]) +INT_UPPER = np.array([6, 2, 2]) # consistency: 0-6 maps via table, realign: 0-2, matrix: 0-2 +INT_NAMES = ["consistency", "realign", "matrix"] + +CONSISTENCY_MAP = [0, 1, 2, 3, 5, 8, 10] +MATRIX_MAP = ["pfasum43", "pfasum60", "protein"] # API values for kalign +MATRIX_DISPLAY = {"pfasum43": "PFASUM43", "pfasum60": "PFASUM60", "protein": "CorBLOSUM66"} + +N_CONT = len(CONT_LOWER) +N_INT = len(INT_LOWER) +N_VAR = N_CONT + N_INT + + +def decode_params(x): + """Decode a parameter vector into a dict of kalign parameters.""" + cont = x[:N_CONT] + ints = np.round(x[N_CONT:]).astype(int) + + consistency_idx = int(np.clip(ints[0], 0, len(CONSISTENCY_MAP) - 1)) + realign_idx = int(np.clip(ints[1], 0, 2)) + matrix_idx = int(np.clip(ints[2], 0, len(MATRIX_MAP) - 1)) + + return { + "gap_open": float(cont[0]), + "gap_extend": float(cont[1]), + "terminal_gap_extend": float(cont[2]), + "vsm_amax": float(cont[3]), + "seq_weights": float(cont[4]), + "consistency_weight": float(cont[5]), + "consistency": CONSISTENCY_MAP[consistency_idx], + "realign": realign_idx, + "seq_type": MATRIX_MAP[matrix_idx], + } + + +def encode_params(params): + """Encode a params dict back into a decision vector (inverse of decode_params).""" + cont = np.array([ + params["gap_open"], params["gap_extend"], params["terminal_gap_extend"], + params["vsm_amax"], params["seq_weights"], params["consistency_weight"], + ]) + consistency_idx = CONSISTENCY_MAP.index(params["consistency"]) + realign_idx = params["realign"] + matrix_idx = MATRIX_MAP.index(params["seq_type"]) + ints = np.array([consistency_idx, realign_idx, matrix_idx], dtype=float) + return np.concatenate([cont, ints]) + + +def _matrix_name(params): + """Human-readable matrix name from seq_type API value.""" + return MATRIX_DISPLAY.get(params["seq_type"], params["seq_type"]) + + +def format_params_short(params): + """Compact one-line summary of a parameter dict.""" + return (f"gpo={params['gap_open']:.1f} gpe={params['gap_extend']:.2f} " + f"tgpe={params['terminal_gap_extend']:.2f} vsm={params['vsm_amax']:.1f} " + f"sw={params['seq_weights']:.1f} c={params['consistency']} " + f"cw={params['consistency_weight']:.1f} re={params['realign']} " + f"{_matrix_name(params)}") + + +def format_params_long(params): + """Verbose one-line summary.""" + return (f"gpo={params['gap_open']:.2f} gpe={params['gap_extend']:.2f} " + f"tgpe={params['terminal_gap_extend']:.2f} vsm={params['vsm_amax']:.2f} " + f"sw={params['seq_weights']:.2f} cons={params['consistency']} " + f"cw={params['consistency_weight']:.2f} re={params['realign']} " + f"mat={_matrix_name(params)}") + + +# --------------------------------------------------------------------------- +# Stratified k-fold CV +# --------------------------------------------------------------------------- + +def stratified_kfold(cases: List[BenchmarkCase], k: int, seed: int = 42 + ) -> List[Tuple[List[BenchmarkCase], List[BenchmarkCase]]]: + """Split cases into k stratified folds by dataset (RV category). + + Returns list of (train, test) pairs. Each fold's test set contains + roughly equal representation from every RV category. + """ + rng = np.random.RandomState(seed) + + # Group by category + by_cat: Dict[str, List[BenchmarkCase]] = defaultdict(list) + for c in cases: + by_cat[c.dataset].append(c) + + # Shuffle within each category and assign fold indices + fold_assignments: Dict[str, int] = {} + for cat_cases in by_cat.values(): + indices = list(range(len(cat_cases))) + rng.shuffle(indices) + for rank, idx in enumerate(indices): + fold_assignments[cat_cases[idx].family] = rank % k + + # Build folds + folds = [] + for fold_idx in range(k): + test = [c for c in cases if fold_assignments[c.family] == fold_idx] + train = [c for c in cases if fold_assignments[c.family] != fold_idx] + folds.append((train, test)) + + return folds + + +# --------------------------------------------------------------------------- +# Evaluation +# --------------------------------------------------------------------------- + +def evaluate_params(params, cases, n_threads=1, quiet=True): + """Run kalign with given params on all cases, return mean metrics. + + Returns dict with keys: f1, tc, recall, precision, wall_time, per_category. + """ + results_by_cat: Dict[str, list] = {} + total_time = 0.0 + + for case in cases: + with tempfile.TemporaryDirectory() as tmpdir: + output = Path(tmpdir) / f"{case.family}_aln.fasta" + + try: + start = time.perf_counter() + kalign.align_file_to_file( + str(case.unaligned), + str(output), + format="fasta", + seq_type=params["seq_type"], + gap_open=params["gap_open"], + gap_extend=params["gap_extend"], + terminal_gap_extend=params["terminal_gap_extend"], + n_threads=n_threads, + vsm_amax=params["vsm_amax"], + seq_weights=params["seq_weights"], + consistency=params["consistency"], + consistency_weight=params["consistency_weight"], + realign=params["realign"], + ) + wall_time = time.perf_counter() - start + total_time += wall_time + + detailed = score_alignment_detailed(case.reference, output) + + cat = case.dataset + if cat not in results_by_cat: + results_by_cat[cat] = [] + results_by_cat[cat].append(detailed) + + except Exception as e: + if not quiet: + print(f" WARN: {case.family}: {e}", file=sys.stderr) + + if not results_by_cat: + return {"f1": 0.0, "tc": 0.0, "recall": 0.0, "precision": 0.0, + "wall_time": total_time, "per_category": {}} + + # Compute per-category means + per_cat = {} + for cat, scores in results_by_cat.items(): + per_cat[cat] = { + "f1": np.mean([s["f1"] for s in scores]), + "tc": np.mean([s["tc"] for s in scores]), + "recall": np.mean([s["recall"] for s in scores]), + "precision": np.mean([s["precision"] for s in scores]), + "n": len(scores), + } + + # Overall means (category-averaged so small categories count equally) + all_f1 = [v["f1"] for v in per_cat.values()] + all_tc = [v["tc"] for v in per_cat.values()] + all_recall = [v["recall"] for v in per_cat.values()] + all_precision = [v["precision"] for v in per_cat.values()] + + return { + "f1": float(np.mean(all_f1)), + "tc": float(np.mean(all_tc)), + "recall": float(np.mean(all_recall)), + "precision": float(np.mean(all_precision)), + "wall_time": total_time, + "per_category": per_cat, + } + + +def evaluate_cv(params, folds, n_threads=1, quiet=True): + """Evaluate params using stratified k-fold CV. + + For each fold, aligns the test cases and scores them. + Returns the mean held-out F1 and TC across folds, + plus per-fold details and total wall time. + """ + fold_f1s = [] + fold_tcs = [] + total_time = 0.0 + + for _, test in folds: + result = evaluate_params(params, test, n_threads, quiet) + fold_f1s.append(result["f1"]) + fold_tcs.append(result["tc"]) + total_time += result["wall_time"] + + return { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, + "fold_tcs": fold_tcs, + "wall_time": total_time, + } + + +# --------------------------------------------------------------------------- +# Rich live dashboard +# --------------------------------------------------------------------------- + +class Dashboard: + """Rich-based live terminal dashboard for optimization progress.""" + + def __init__(self, n_gen: int, pop_size: int, baseline_f1: float, baseline_tc: float, + optimize_time: bool = False, baseline_time: float = 0.0): + self.n_gen = n_gen + self.pop_size = pop_size + self.baseline_f1 = baseline_f1 + self.baseline_tc = baseline_tc + self.optimize_time = optimize_time + self.baseline_time = baseline_time + self.console = Console() + + # State + self.current_gen = 0 + self.eval_count = 0 + self.total_evals = n_gen * pop_size + self.gen_start_time = time.time() + self.run_start_time = time.time() + self.current_eval_params: Optional[dict] = None + self.current_eval_idx = 0 # within generation + + # Best-ever tracking + self.best_f1 = 0.0 + self.best_f1_params: Optional[dict] = None + self.best_tc = 0.0 + self.best_tc_params: Optional[dict] = None + self.best_time = float("inf") + self.best_time_params: Optional[dict] = None + + # Pareto front (updated per generation) + self.pareto_front: List[dict] = [] + + # Generation history (best F1 per gen for trend) + self.gen_history: List[dict] = [] + + # Recent evaluations (ring buffer for last 5) + self.recent_evals: List[dict] = [] + + # Progress bar + self.progress = Progress( + SpinnerColumn(), + TextColumn("[bold blue]{task.description}"), + BarColumn(bar_width=40), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TimeElapsedColumn(), + ) + self.gen_task = self.progress.add_task("Generation", total=n_gen) + self.eval_task = self.progress.add_task(" Eval", total=pop_size) + + self.live = Live(self._build_layout(), console=self.console, refresh_per_second=2) + + def start(self): + self.run_start_time = time.time() + self.live.start() + + def stop(self): + self.live.stop() + + def _delta_str(self, value: float, baseline: float) -> str: + """Format a value with delta vs baseline, colored.""" + delta = value - baseline + if delta > 0.005: + return f"[bold green]{value:.4f} (+{delta:.4f})[/]" + elif delta < -0.005: + return f"[bold red]{value:.4f} ({delta:.4f})[/]" + else: + return f"[dim]{value:.4f} ({delta:+.4f})[/]" + + def _build_status_panel(self) -> Panel: + elapsed = time.time() - self.run_start_time + gen_elapsed = time.time() - self.gen_start_time + + if self.eval_count > 0: + avg_per_eval = elapsed / self.eval_count + remaining = avg_per_eval * (self.total_evals - self.eval_count) + eta_str = f"{remaining / 60:.0f}m" if remaining > 60 else f"{remaining:.0f}s" + else: + eta_str = "..." + + lines = [ + f"Gen [bold]{self.current_gen}[/]/{self.n_gen} " + f"Eval [bold]{self.eval_count}[/]/{self.total_evals} " + f"Elapsed [bold]{elapsed / 60:.1f}m[/] " + f"ETA [bold]{eta_str}[/] " + f"Gen time [bold]{gen_elapsed:.1f}s[/]", + ] + + if self.current_eval_params: + lines.append(f"[dim]Current: {format_params_short(self.current_eval_params)}[/]") + + return Panel("\n".join(lines), title="Progress", border_style="blue") + + def _build_best_panel(self) -> Panel: + lines = [] + baseline_str = f"[dim]Baseline: F1={self.baseline_f1:.4f} TC={self.baseline_tc:.4f}" + if self.optimize_time: + baseline_str += f" Time={self.baseline_time:.1f}s" + baseline_str += "[/]" + lines.append(baseline_str) + lines.append("") + + if self.best_f1_params: + lines.append(f"Best F1: {self._delta_str(self.best_f1, self.baseline_f1)}") + lines.append(f" [dim]{format_params_short(self.best_f1_params)}[/]") + else: + lines.append("[dim]Best F1: (no evaluations yet)[/]") + + lines.append("") + + if self.best_tc_params: + lines.append(f"Best TC: {self._delta_str(self.best_tc, self.baseline_tc)}") + lines.append(f" [dim]{format_params_short(self.best_tc_params)}[/]") + else: + lines.append("[dim]Best TC: (no evaluations yet)[/]") + + if self.optimize_time: + lines.append("") + if self.best_time_params: + delta = self.best_time - self.baseline_time + if delta < -1.0: + time_str = f"[bold green]{self.best_time:.1f}s ({delta:.1f}s)[/]" + elif delta > 1.0: + time_str = f"[bold red]{self.best_time:.1f}s (+{delta:.1f}s)[/]" + else: + time_str = f"[dim]{self.best_time:.1f}s ({delta:+.1f}s)[/]" + lines.append(f"Fastest: {time_str}") + lines.append(f" [dim]{format_params_short(self.best_time_params)}[/]") + + return Panel("\n".join(lines), title="Best Solutions (CV)", border_style="green") + + def _build_pareto_table(self) -> Panel: + table = Table(title="Pareto Front", expand=True, show_lines=False, padding=(0, 1)) + table.add_column("#", style="dim", width=3) + table.add_column("CV F1", justify="right", width=8) + table.add_column("CV TC", justify="right", width=8) + if self.optimize_time: + table.add_column("Time", justify="right", width=7) + table.add_column("gpo", justify="right", width=5) + table.add_column("gpe", justify="right", width=5) + table.add_column("tgpe", justify="right", width=5) + table.add_column("vsm", justify="right", width=4) + table.add_column("sw", justify="right", width=4) + table.add_column("c", justify="right", width=2) + table.add_column("cw", justify="right", width=4) + table.add_column("re", justify="right", width=2) + table.add_column("mat", width=8) + + n_cols = 13 if self.optimize_time else 12 + + # Sort by F1 descending + sorted_front = sorted(self.pareto_front, key=lambda x: -x["f1"]) + + for i, sol in enumerate(sorted_front[:10]): # show top 10 + p = sol["params"] + f1_style = "bold green" if sol["f1"] > self.baseline_f1 else "" + tc_style = "bold green" if sol["tc"] > self.baseline_tc else "" + row = [ + str(i), + Text(f"{sol['f1']:.4f}", style=f1_style), + Text(f"{sol['tc']:.4f}", style=tc_style), + ] + if self.optimize_time: + wt = sol.get("wall_time", 0.0) + row.append(f"{wt:.1f}s" if wt else "?") + row.extend([ + f"{p['gap_open']:.1f}", + f"{p['gap_extend']:.2f}", + f"{p['terminal_gap_extend']:.2f}", + f"{p['vsm_amax']:.1f}", + f"{p['seq_weights']:.1f}", + str(p['consistency']), + f"{p['consistency_weight']:.1f}", + str(p['realign']), + _matrix_name(p), + ]) + table.add_row(*row) + + if not self.pareto_front: + table.add_row(*[""] * n_cols) + + return Panel(table, border_style="cyan") + + def _build_trend_panel(self) -> Panel: + if not self.gen_history: + return Panel("[dim]No generation data yet[/]", title="Trend", border_style="yellow") + + lines = [] + # Show sparkline-style trend using simple chars + bar_width = 30 + if len(self.gen_history) > 1: + f1_vals = [g["best_f1"] for g in self.gen_history] + lo = min(min(f1_vals), self.baseline_f1) - 0.01 + hi = max(max(f1_vals), self.baseline_f1) + 0.01 + rng = hi - lo if hi > lo else 1.0 + + # Baseline marker position + bl_pos = int((self.baseline_f1 - lo) / rng * bar_width) + + lines.append("[bold]Best CV F1 per generation:[/]") + for g in self.gen_history[-12:]: # last 12 gens + pos = int((g["best_f1"] - lo) / rng * bar_width) + bar = list("·" * bar_width) + bar[min(bl_pos, bar_width - 1)] = "│" # baseline marker + for j in range(min(pos, bar_width)): + bar[j] = "█" + bar_str = "".join(bar) + delta = g["best_f1"] - self.baseline_f1 + color = "green" if delta > 0 else "red" if delta < -0.005 else "dim" + lines.append(f" Gen {g['gen']:>3d} [{color}]{bar_str} {g['best_f1']:.4f}[/]") + + lines.append(f" [dim]│ = baseline ({self.baseline_f1:.4f})[/]") + else: + g = self.gen_history[0] + lines.append(f"Gen {g['gen']}: F1={g['best_f1']:.4f} TC={g['best_tc']:.4f}") + + return Panel("\n".join(lines), title="Trend", border_style="yellow") + + def _build_recent_panel(self) -> Panel: + if not self.recent_evals: + return Panel("[dim]No evaluations yet[/]", title="Recent", border_style="dim") + + lines = [] + for ev in self.recent_evals[-5:]: + f1 = ev["f1"] + tc = ev["tc"] + delta = f1 - self.baseline_f1 + color = "green" if delta > 0 else "red" if delta < -0.005 else "dim" + time_str = f" t={ev['wall_time']:.1f}s" if self.optimize_time else "" + lines.append( + f" [{color}]F1={f1:.4f} TC={tc:.4f}{time_str}[/] " + f"[dim]{format_params_short(ev['params'])}[/]" + ) + return Panel("\n".join(lines), title="Recent Evaluations", border_style="dim") + + def _build_layout(self): + layout = Layout() + layout.split_column( + Layout(self.progress, name="progress", size=3), + Layout(name="top", size=7), + Layout(name="middle"), + Layout(self._build_recent_panel(), name="bottom", size=8), + ) + layout["top"].split_row( + Layout(self._build_status_panel(), name="status"), + Layout(self._build_best_panel(), name="best"), + ) + layout["middle"].split_row( + Layout(self._build_pareto_table(), name="pareto", ratio=3), + Layout(self._build_trend_panel(), name="trend", ratio=2), + ) + return layout + + def refresh(self): + self.live.update(self._build_layout()) + + def on_eval_start(self, params: dict, eval_num: int, eval_in_gen: int): + self.eval_count = eval_num + self.current_eval_idx = eval_in_gen + self.current_eval_params = params + self.progress.update(self.eval_task, completed=eval_in_gen) + self.refresh() + + def on_eval_end(self, params: dict, cv_result: dict): + f1 = cv_result["f1"] + tc = cv_result["tc"] + wt = cv_result.get("wall_time", 0.0) + + self.recent_evals.append({"params": params, "f1": f1, "tc": tc, "wall_time": wt}) + if len(self.recent_evals) > 5: + self.recent_evals.pop(0) + + if f1 > self.best_f1: + self.best_f1 = f1 + self.best_f1_params = params + if tc > self.best_tc: + self.best_tc = tc + self.best_tc_params = params + if self.optimize_time and wt < self.best_time and f1 > 0.5: + self.best_time = wt + self.best_time_params = params + + self.refresh() + + def on_gen_start(self, gen: int): + self.current_gen = gen + self.gen_start_time = time.time() + self.progress.update(self.gen_task, completed=gen) + self.progress.update(self.eval_task, completed=0) + self.refresh() + + def on_gen_end(self, gen: int, pareto_front: List[dict]): + self.pareto_front = pareto_front + + best_f1_in_gen = max((s["f1"] for s in pareto_front), default=0.0) + best_tc_in_gen = max((s["tc"] for s in pareto_front), default=0.0) + self.gen_history.append({ + "gen": gen, + "best_f1": best_f1_in_gen, + "best_tc": best_tc_in_gen, + "n_pareto": len(pareto_front), + }) + + self.progress.update(self.gen_task, completed=gen) + self.refresh() + + +# --------------------------------------------------------------------------- +# Parallel evaluation helper (must be top-level for pickling) +# --------------------------------------------------------------------------- + +def _eval_one(args_tuple): + """Evaluate one parameter vector. Top-level function for ProcessPoolExecutor.""" + x, folds, n_threads = args_tuple + params = decode_params(x) + cv_result = evaluate_cv(params, folds, n_threads, quiet=True) + return params, cv_result + + +def _eval_one_fold(args_tuple): + """Evaluate one (individual, fold) pair. Finer-grained parallelism.""" + ind_idx, fold_idx, x, test_cases, n_threads = args_tuple + params = decode_params(x) + result = evaluate_params(params, test_cases, n_threads, quiet=True) + return ind_idx, fold_idx, params, result + + +def _kill_pool(pool: ProcessPoolExecutor) -> None: + """Forcefully terminate all worker processes in the pool.""" + # Access the internal process map to send SIGTERM to each worker + for pid in list(pool._processes): # noqa: SLF001 + try: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + pool.shutdown(wait=False, cancel_futures=True) + + +# --------------------------------------------------------------------------- +# pymoo Problem + Callback +# --------------------------------------------------------------------------- + +class KalignCVProblem(Problem): + """Multi-objective optimization with stratified CV evaluation.""" + + def __init__(self, folds, n_threads=1, n_workers=1, optimize_time=False, + dashboard: Optional[Dashboard] = None): + n_obj = 3 if optimize_time else 2 + xl = np.concatenate([CONT_LOWER, INT_LOWER.astype(float)]) + xu = np.concatenate([CONT_UPPER, INT_UPPER.astype(float)]) + + super().__init__( + n_var=N_VAR, + n_obj=n_obj, + xl=xl, + xu=xu, + ) + self.folds = folds + self.n_threads = n_threads + self.n_workers = n_workers + self.optimize_time = optimize_time + self.dashboard = dashboard + self.eval_count = 0 + self.history: List[dict] = [] + + def _evaluate(self, X, out, *args, **kwargs): # pyright: ignore[reportUnusedVariable] + F = np.zeros((X.shape[0], self.n_obj)) + + if self.n_workers > 1: + self._evaluate_parallel(X, F) + else: + self._evaluate_serial(X, F) + + out["F"] = F + + def _evaluate_serial(self, X, F): + for i, x in enumerate(X): + params = decode_params(x) + self.eval_count += 1 + + if self.dashboard: + self.dashboard.on_eval_start(params, self.eval_count, i) + + cv_result = evaluate_cv(params, self.folds, self.n_threads, quiet=True) + self._record(i, F, params, cv_result) + + def _evaluate_parallel(self, X, F): + """Fine-grained parallelism: submit (individual × fold) jobs. + + For pop_size=60, k=5 folds → 300 jobs, much better load balancing + than 60 coarse-grained jobs where slow individuals block the generation. + """ + n_folds = len(self.folds) + n_pop = X.shape[0] + + # Build flat job list: (ind_idx, fold_idx, x, test_cases, n_threads) + jobs = [] + for i, x in enumerate(X): + for fold_idx, (_, test) in enumerate(self.folds): + jobs.append((i, fold_idx, x, test, self.n_threads)) + + if self.dashboard: + self.dashboard.on_eval_start( + {"gap_open": 0, "gap_extend": 0, "terminal_gap_extend": 0, + "vsm_amax": 0, "seq_weights": 0, "consistency_weight": 0, + "consistency": 0, "realign": 0, "seq_type": "..."}, + self.eval_count + 1, 0) + + # Collect per-fold results: fold_results[ind_idx] = {fold_idx: result} + fold_results: Dict[int, Dict[int, dict]] = defaultdict(dict) + ind_params: Dict[int, dict] = {} + + pool = ProcessPoolExecutor(max_workers=self.n_workers) + try: + futures = {pool.submit(_eval_one_fold, j): j[:2] for j in jobs} + completed_individuals = set() + completed_jobs = 0 + + for future in as_completed(futures): + ind_idx, fold_idx, params, result = future.result() + fold_results[ind_idx][fold_idx] = result + ind_params[ind_idx] = params + completed_jobs += 1 + + # Check if this individual now has all folds complete + if len(fold_results[ind_idx]) == n_folds: + completed_individuals.add(ind_idx) + self.eval_count += 1 + + # Aggregate fold results into CV score + fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] + fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] + total_time = sum(fold_results[ind_idx][fi]["wall_time"] for fi in range(n_folds)) + + cv_result = { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, + "fold_tcs": fold_tcs, + "wall_time": total_time, + } + + self._record(ind_idx, F, params, cv_result) + + if self.dashboard: + self.dashboard.on_eval_start( + params, self.eval_count, len(completed_individuals)) + + except KeyboardInterrupt: + _kill_pool(pool) + raise + finally: + pool.shutdown(wait=False) + + def _record(self, i, F, params, cv_result): + F[i, 0] = -cv_result["f1"] + F[i, 1] = -cv_result["tc"] + if self.optimize_time: + F[i, 2] = cv_result["wall_time"] + + self.history.append({ + "eval": self.eval_count, + "params": params, + "cv_result": cv_result, + }) + + if self.dashboard: + self.dashboard.on_eval_end(params, cv_result) + + +class GenerationCallback(Callback): + """pymoo callback: updates dashboard + saves checkpoint after each generation.""" + + def __init__(self, dashboard: Optional[Dashboard] = None, + checkpoint_path: Optional[Path] = None, + problem: Optional["KalignCVProblem"] = None): + super().__init__() + self.dashboard = dashboard + self.checkpoint_path = checkpoint_path + self.problem = problem + + def notify(self, algorithm): + gen = algorithm.n_gen + + if self.dashboard: + self.dashboard.on_gen_start(gen) + + # Extract Pareto front from algorithm.opt + pareto = [] + if algorithm.opt is not None and len(algorithm.opt) > 0: + for ind in algorithm.opt: + params = decode_params(ind.X) + entry = { + "params": params, + "f1": -ind.F[0], + "tc": -ind.F[1], + } + if len(ind.F) > 2: + entry["wall_time"] = ind.F[2] + pareto.append(entry) + + if self.dashboard: + self.dashboard.on_gen_end(gen, pareto) + + # Save checkpoint after every generation + if self.checkpoint_path: + # Save population state (X, F) — not the algorithm object (has unpicklable locks) + pop = algorithm.pop + ckpt = { + "pop_X": pop.get("X").copy(), + "pop_F": pop.get("F").copy(), + "n_gen_completed": gen, + "history": self.problem.history if self.problem else [], + "pop_size": len(pop), + } + # Write to temp file then rename for atomicity + tmp = self.checkpoint_path.with_suffix(".tmp") + with open(tmp, "wb") as f: + pickle.dump(ckpt, f) + tmp.rename(self.checkpoint_path) + + +def load_checkpoint(path: Path): + """Load a generation checkpoint. Returns (pop_X, pop_F, n_gen_completed, history).""" + with open(path, "rb") as f: + ckpt = pickle.load(f) # noqa: S301 + return ckpt["pop_X"], ckpt["pop_F"], ckpt["n_gen_completed"], ckpt.get("history", []) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Optimize kalign hyperparameters with NSGA-II + stratified k-fold CV") + parser.add_argument("--pop-size", type=int, default=40, + help="Population size (default: 40)") + parser.add_argument("--n-gen", type=int, default=50, + help="Number of generations (default: 50)") + parser.add_argument("--n-folds", type=int, default=5, + help="Number of CV folds (default: 5)") + parser.add_argument("--n-threads", type=int, default=1, + help="OpenMP threads per kalign alignment (default: 1)") + parser.add_argument("--n-workers", type=int, default=1, + help="Parallel worker processes for evaluating individuals (default: 1)") + parser.add_argument("--seed", type=int, default=42, + help="Random seed (default: 42)") + parser.add_argument("--optimize-time", action="store_true", + help="Add wall time as 3rd objective") + parser.add_argument("--output-dir", type=str, default="benchmarks/results/optim", + help="Output directory for results") + parser.add_argument("--run-name", type=str, default=None, + help="Name for this run (creates subdirectory, e.g. 'run2_time')") + parser.add_argument("--no-dashboard", action="store_true", + help="Disable rich dashboard, use plain text output") + parser.add_argument("--resume", type=str, default=None, + help="Resume from a generation checkpoint file (.pkl)") + args = parser.parse_args() + + console = Console() + + # Setup + if not balibase_is_available(): + console.print("Downloading BAliBASE...") + balibase_download() + + cases = balibase_cases() + console.print(f"Loaded [bold]{len(cases)}[/] BAliBASE cases") + + # Show category distribution + cats: Dict[str, int] = {} + for c in cases: + cats[c.dataset] = cats.get(c.dataset, 0) + 1 + for cat, n in sorted(cats.items()): + console.print(f" {cat}: {n} cases") + + # Build stratified folds + k = args.n_folds + folds = stratified_kfold(cases, k, seed=args.seed) + console.print(f"\nStratified [bold]{k}[/]-fold CV:") + for i, (train, test) in enumerate(folds): + test_cats: Dict[str, int] = defaultdict(int) + for c in test: + test_cats[c.dataset] += 1 + cat_str = ", ".join(f"{cat.replace('balibase_', '')}:{n}" + for cat, n in sorted(test_cats.items())) + console.print(f" Fold {i}: {len(test)} test / {len(train)} train ({cat_str})") + + output_dir = Path(args.output_dir) + if args.run_name: + output_dir = output_dir / args.run_name + output_dir.mkdir(parents=True, exist_ok=True) + + # --- Baseline evaluation (CV) --- parallelized across folds + # Best F1 from first optimization run (pop_size=60, n_gen=100) + console.print(f"\n[bold]Baseline evaluation[/] (best from run 1, {k}-fold CV)") + baseline_params = { + "gap_open": 8.472, "gap_extend": 0.554, "terminal_gap_extend": 0.409, + "vsm_amax": 1.359, "seq_weights": 3.407, "consistency_weight": 1.167, + "consistency": 8, "realign": 2, "seq_type": "pfasum60", + } + n_baseline_workers = max(1, args.n_workers) + if n_baseline_workers > 1: + # Run CV folds + full-dataset eval in parallel + baseline_jobs = [] + for fi, (_, test) in enumerate(folds): + baseline_jobs.append((0, fi, encode_params(baseline_params), test, args.n_threads)) + # Add full-dataset as an extra "fold" + baseline_jobs.append((0, k, encode_params(baseline_params), cases, args.n_threads)) + console.print(f" Running {len(baseline_jobs)} baseline jobs in parallel ({n_baseline_workers} workers)...") + with ProcessPoolExecutor(max_workers=min(n_baseline_workers, len(baseline_jobs))) as pool: + futures = {pool.submit(_eval_one_fold, j): j[1] for j in baseline_jobs} + fold_results_bl = {} + for future in as_completed(futures): + _, fi, _, result = future.result() + fold_results_bl[fi] = result + fold_f1s = [fold_results_bl[fi]["f1"] for fi in range(k)] + fold_tcs = [fold_results_bl[fi]["tc"] for fi in range(k)] + total_time = sum(fold_results_bl[fi]["wall_time"] for fi in range(k)) + baseline_cv = { + "f1": float(np.mean(fold_f1s)), "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, "fold_tcs": fold_tcs, "wall_time": total_time, + } + baseline_full = fold_results_bl[k] # the extra "fold" with all cases + else: + baseline_cv = evaluate_cv(baseline_params, folds, args.n_threads) + baseline_full = evaluate_params(baseline_params, cases, args.n_threads) + + console.print(f" CV F1=[bold]{baseline_cv['f1']:.4f}[/]±{baseline_cv['f1_std']:.4f} " + f"CV TC=[bold]{baseline_cv['tc']:.4f}[/]±{baseline_cv['tc_std']:.4f} " + f"time={baseline_cv['wall_time']:.1f}s") + for i, (f1, tc) in enumerate(zip(baseline_cv['fold_f1s'], baseline_cv['fold_tcs'])): + console.print(f" Fold {i}: F1={f1:.4f} TC={tc:.4f}") + + console.print(f" Full-data F1=[bold]{baseline_full['f1']:.4f}[/] " + f"TC=[bold]{baseline_full['tc']:.4f}[/]") + for cat, v in sorted(baseline_full["per_category"].items()): + console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") + + # --- Optimization --- + n_evals = args.pop_size * args.n_gen + est_sec_per_eval = baseline_cv["wall_time"] + parallelism = max(1, args.n_workers) + est_hours = n_evals * est_sec_per_eval / parallelism / 3600 + + console.print(f"\n[bold]Starting NSGA-II[/]: pop_size={args.pop_size}, n_gen={args.n_gen}, " + f"{k}-fold CV, {args.n_workers} worker(s) × {args.n_threads} thread(s)") + console.print(f"Total evaluations: ~{n_evals} ({n_evals * len(cases)} alignments)") + console.print(f"Estimated time: ~{est_hours:.1f} hours " + f"(~{est_sec_per_eval:.0f}s per eval, " + f"{parallelism}× parallel)\n") + + # Set up dashboard or plain mode + use_dashboard = not args.no_dashboard + dashboard = None + + if use_dashboard: + dashboard = Dashboard( + n_gen=args.n_gen, + pop_size=args.pop_size, + baseline_f1=baseline_cv["f1"], + baseline_tc=baseline_cv["tc"], + optimize_time=args.optimize_time, + baseline_time=baseline_cv["wall_time"], + ) + + problem = KalignCVProblem( + folds=folds, + n_threads=args.n_threads, + n_workers=args.n_workers, + optimize_time=args.optimize_time, + dashboard=dashboard, + ) + + checkpoint_path = output_dir / "gen_checkpoint.pkl" + callback = GenerationCallback( + dashboard=dashboard, + checkpoint_path=checkpoint_path, + problem=problem, + ) + + # Resume from checkpoint or start fresh + resumed_gen = 0 + if args.resume: + resume_path = Path(args.resume) + if not resume_path.exists(): + console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") + return + pop_X, _pop_F, resumed_gen, prev_history = load_checkpoint(resume_path) + problem.history = prev_history + console.print(f"[bold green]Resumed[/] from generation {resumed_gen} " + f"({len(prev_history)} prior evaluations)") + remaining = args.n_gen - resumed_gen + if remaining <= 0: + console.print(f"[bold yellow]Already completed {resumed_gen} generations " + f"(requested {args.n_gen}). Increase --n-gen to continue.[/]") + return + termination = get_termination("n_gen", remaining) + # Reconstruct algorithm with saved population as initial sampling + algorithm = NSGA2( + pop_size=len(pop_X), + sampling=pop_X, + crossover=SBX(prob=0.9, eta=15), + mutation=PM(eta=20), + eliminate_duplicates=True, + ) + else: + algorithm = NSGA2( + pop_size=args.pop_size, + sampling=LHS(), + crossover=SBX(prob=0.9, eta=15), + mutation=PM(eta=20), + eliminate_duplicates=True, + ) + termination = get_termination("n_gen", args.n_gen) + + if dashboard: + dashboard.start() + + try: + res = minimize( + problem, + algorithm, + termination, + seed=args.seed, + verbose=not use_dashboard, # disable pymoo's own output when dashboard is active + callback=callback, + ) + except KeyboardInterrupt: + if dashboard: + dashboard.stop() + console.print("\n[bold yellow]Interrupted![/] Checkpoint was saved after last " + f"completed generation to:\n {checkpoint_path}") + console.print("Resume with: [bold]--resume " + str(checkpoint_path) + "[/]") + os._exit(1) # noqa: SLF001 — skip atexit handlers that hang on worker join + finally: + if dashboard: + dashboard.stop() + + # --- Results --- + console.print(f"\n[bold]Optimization complete.[/] " + f"{len(res.F)} Pareto-optimal solutions found.\n") + + pareto_configs = [] + for i, (x, f) in enumerate(zip(res.X, res.F)): + params = decode_params(x) + f1 = -f[0] + tc = -f[1] + wt = f[2] if args.optimize_time else None + pareto_configs.append({"params": params, "f1_cv": f1, "tc_cv": tc, "wall_time": wt}) + + # Print Pareto front as a rich table + table = Table(title="Pareto Front (sorted by CV F1)") + table.add_column("#", style="dim", width=3) + table.add_column("CV F1", justify="right") + table.add_column("CV TC", justify="right") + table.add_column("Parameters") + + sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) + for i, cfg in enumerate(sorted_pareto): + f1_style = "bold green" if cfg["f1_cv"] > baseline_cv["f1"] else "" + tc_style = "bold green" if cfg["tc_cv"] > baseline_cv["tc"] else "" + table.add_row( + str(i), + Text(f"{cfg['f1_cv']:.4f}", style=f1_style), + Text(f"{cfg['tc_cv']:.4f}", style=tc_style), + format_params_short(cfg["params"]), + ) + console.print(table) + + # --- Re-evaluate best on FULL dataset --- + best_f1_idx = np.argmin(res.F[:, 0]) + best = pareto_configs[best_f1_idx] + + console.print(f"\n{'='*60}") + console.print(f"[bold]Best CV F1 solution:[/] CV F1={best['f1_cv']:.4f} CV TC={best['tc_cv']:.4f}") + console.print(f" {format_params_long(best['params'])}") + + console.print(f"\n[bold]Full-dataset evaluation[/] (checking for overfit)") + best_full = evaluate_params(best["params"], cases, args.n_threads) + console.print(f" Full F1=[bold]{best_full['f1']:.4f}[/] Full TC=[bold]{best_full['tc']:.4f}[/] " + f"Recall={best_full['recall']:.4f} Precision={best_full['precision']:.4f}") + for cat, v in sorted(best_full["per_category"].items()): + console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") + + gap_f1 = best_full["f1"] - best["f1_cv"] + gap_tc = best_full["tc"] - best["tc_cv"] + console.print(f"\n Overfit check (full - CV): F1 {gap_f1:+.4f} TC {gap_tc:+.4f}") + if gap_f1 > 0.02: + console.print(" [bold yellow]Warning:[/] Full-data F1 notably higher than CV — possible overfit") + else: + console.print(" [bold green]OK:[/] Full-data and CV scores are consistent") + + # Comparison vs baseline + console.print(f"\n[bold]Improvement vs baseline[/] (full dataset)") + comp_table = Table() + comp_table.add_column("Category") + comp_table.add_column("F1 before", justify="right") + comp_table.add_column("F1 after", justify="right") + comp_table.add_column("ΔF1", justify="right") + comp_table.add_column("TC before", justify="right") + comp_table.add_column("TC after", justify="right") + comp_table.add_column("ΔTC", justify="right") + + for cat in sorted(set(list(baseline_full["per_category"].keys()) + + list(best_full["per_category"].keys()))): + b = baseline_full["per_category"].get(cat, {"f1": 0.0, "tc": 0.0}) + o = best_full["per_category"].get(cat, {"f1": 0.0, "tc": 0.0}) + df1 = o["f1"] - b["f1"] + dtc = o["tc"] - b["tc"] + f1_style = "green" if df1 > 0.005 else "red" if df1 < -0.005 else "" + tc_style = "green" if dtc > 0.005 else "red" if dtc < -0.005 else "" + comp_table.add_row( + cat.replace("balibase_", ""), + f"{b['f1']:.4f}", f"{o['f1']:.4f}", + Text(f"{df1:+.4f}", style=f1_style), + f"{b['tc']:.4f}", f"{o['tc']:.4f}", + Text(f"{dtc:+.4f}", style=tc_style), + ) + + df1 = best_full["f1"] - baseline_full["f1"] + dtc = best_full["tc"] - baseline_full["tc"] + f1_style = "bold green" if df1 > 0.005 else "bold red" if df1 < -0.005 else "bold" + tc_style = "bold green" if dtc > 0.005 else "bold red" if dtc < -0.005 else "bold" + comp_table.add_row( + "[bold]OVERALL[/]", + f"{baseline_full['f1']:.4f}", f"{best_full['f1']:.4f}", + Text(f"{df1:+.4f}", style=f1_style), + f"{baseline_full['tc']:.4f}", f"{best_full['tc']:.4f}", + Text(f"{dtc:+.4f}", style=tc_style), + ) + console.print(comp_table) + + # --- Save --- + checkpoint = { + "pareto_configs": pareto_configs, + "history": problem.history, + "baseline_cv": baseline_cv, + "baseline_full": baseline_full, + "best_full": best_full, + "folds_info": [(len(tr), len(te)) for tr, te in folds], + "args": vars(args), + } + ckpt_path = output_dir / "optim_checkpoint.pkl" + with open(ckpt_path, "wb") as f: + pickle.dump(checkpoint, f) + console.print(f"\nCheckpoint saved to {ckpt_path}") + + summary_path = output_dir / "pareto_front.txt" + with open(summary_path, "w") as f: + f.write("# Pareto-optimal kalign configs (NSGA-II + {}-fold stratified CV)\n".format(k)) + f.write(f"# pop_size={args.pop_size} n_gen={args.n_gen} " + f"n_cases={len(cases)} seed={args.seed}\n") + f.write(f"# Baseline CV: F1={baseline_cv['f1']:.4f} TC={baseline_cv['tc']:.4f}\n\n") + for i, cfg in enumerate(sorted_pareto): + p = cfg["params"] + f.write(f"[{i}] CV_F1={cfg['f1_cv']:.4f} CV_TC={cfg['tc_cv']:.4f}\n") + f.write(f" gap_open={p['gap_open']:.3f}\n") + f.write(f" gap_extend={p['gap_extend']:.3f}\n") + f.write(f" terminal_gap_extend={p['terminal_gap_extend']:.3f}\n") + f.write(f" vsm_amax={p['vsm_amax']:.3f}\n") + f.write(f" seq_weights={p['seq_weights']:.3f}\n") + f.write(f" consistency={p['consistency']}\n") + f.write(f" consistency_weight={p['consistency_weight']:.3f}\n") + f.write(f" realign={p['realign']}\n") + f.write(f" matrix={_matrix_name(p)}\n\n") + console.print(f"Pareto front saved to {summary_path}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/optimize_unified.py b/benchmarks/optimize_unified.py new file mode 100644 index 0000000..cf2a4b3 --- /dev/null +++ b/benchmarks/optimize_unified.py @@ -0,0 +1,1500 @@ +#!/usr/bin/env python3 +"""Unified multi-objective hyperparameter optimization for kalign using pymoo. + +Searches across kalign's entire operating range: from fast single-run alignment +through consistency-enhanced single-run to multi-run ensemble with POAR consensus. +Always uses three objectives: F1, TC, and wall time. The resulting 3D Pareto +surface reveals the full speed/accuracy trade-off landscape in one run. + +Objectives (always 3): + 1. Maximize F1 (category-averaged, held-out CV) -> pymoo minimizes -F1 + 2. Maximize TC (category-averaged, held-out CV) -> pymoo minimizes -TC + 3. Minimize wall time (total CV evaluation time in seconds) + +Per-run decision variables (x max_runs slots): + - gpo: gap open penalty [2.0, 15.0] + - gpe: gap extend penalty [0.5, 5.0] + - tgpe: terminal gap extend [0.1, 3.0] + - noise: tree perturbation sigma [0.0, 0.5] + - matrix: substitution matrix {PFASUM43, PFASUM60, CorBLOSUM66} + +Shared decision variables: + - n_runs: {1, 3, 5} Core mode variable + - vsm_amax: [0.0, 5.0] Variable scoring matrix + - seq_weights: [0.0, 5.0] Profile rebalancing + - consistency: {0..6} Anchor consistency rounds + - consistency_weight: [0.5, 5.0] Consistency bonus weight + - realign: {0, 1, 2} Tree-rebuild iterations + - refine: {0, 1, 2, 3} Post-alignment refinement + - min_support: {0..max_runs} POAR consensus threshold + +Usage: + # Quick smoke test (protein/BAliBASE, default) + uv run python -m benchmarks.optimize_unified --pop-size 20 --n-gen 5 + + # Production run on BAliBASE (protein) + uv run python -m benchmarks.optimize_unified \\ + --pop-size 200 --n-gen 100 --n-workers 56 --n-threads 1 + + # Production run on BRAliBASE (RNA) + uv run python -m benchmarks.optimize_unified --dataset bralibase \\ + --pop-size 200 --n-gen 100 --n-workers 56 --n-threads 1 + + # Resume + uv run python -m benchmarks.optimize_unified \\ + --resume benchmarks/results/unified_optim/balibase/gen_checkpoint.pkl \\ + --n-gen 150 --n-workers 56 +""" + +import argparse +import os +import pickle +import signal +import sys +import tempfile +import time +from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import numpy as np + +try: + from pymoo.algorithms.moo.nsga3 import NSGA3 # type: ignore[import-untyped] + from pymoo.core.callback import Callback # type: ignore[import-untyped] + from pymoo.core.mixed import ( # type: ignore[import-untyped] + MixedVariableDuplicateElimination, + MixedVariableMating, + MixedVariableSampling, + ) + from pymoo.core.population import Population # type: ignore[import-untyped] + from pymoo.core.problem import Problem # type: ignore[import-untyped] + from pymoo.core.variable import Choice, Integer, Real # type: ignore[import-untyped] + from pymoo.optimize import minimize # type: ignore[import-untyped] + from pymoo.termination import get_termination # type: ignore[import-untyped] + from pymoo.util.ref_dirs import get_reference_directions # type: ignore[import-untyped] +except ImportError: + print("pymoo not installed. Run: uv pip install pymoo") + sys.exit(1) + +from rich.console import Console # type: ignore[import-untyped] +from rich.layout import Layout # type: ignore[import-untyped] +from rich.live import Live # type: ignore[import-untyped] +from rich.panel import Panel # type: ignore[import-untyped] +from rich.progress import ( # type: ignore[import-untyped] + BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, +) +from rich.table import Table # type: ignore[import-untyped] +from rich.text import Text # type: ignore[import-untyped] + +from kalign._core import ( # type: ignore[import-untyped] + PROTEIN, PROTEIN_PFASUM43, PROTEIN_PFASUM60, DNA, RNA, + REFINE_NONE, REFINE_ALL, + REFINE_CONFIDENT, REFINE_INLINE, ensemble_custom_file_to_file, +) +import kalign # type: ignore[import-untyped] + +from .datasets import (BenchmarkCase, + balibase_cases, balibase_download, balibase_is_available, + bralibase_cases, bralibase_download, bralibase_is_available, + mdsa_cases, mdsa_download, mdsa_is_available) +from .scoring import score_alignment_detailed + +# --------------------------------------------------------------------------- +# Parameter space definition +# --------------------------------------------------------------------------- + +# Maps for categorical variables (Choice options) +# These define the actual values; pymoo Choice handles selection directly. + +N_RUNS_MAP = [1, 3, 5] # index -> actual n_runs +CONSISTENCY_MAP = [0, 1, 2, 3, 5, 8, 10] +REFINE_MAP = [REFINE_NONE, REFINE_ALL, REFINE_CONFIDENT, REFINE_INLINE] +REFINE_NAMES = {REFINE_NONE: "N", REFINE_ALL: "A", REFINE_CONFIDENT: "C", REFINE_INLINE: "I"} +REFINE_LONG = {REFINE_NONE: "NONE", REFINE_ALL: "ALL", REFINE_CONFIDENT: "CONFIDENT", + REFINE_INLINE: "INLINE"} + +# --- Dataset-specific parameter space profiles --- + +PARAM_PROFILES = { + "protein": { + "per_run_cont_lower": np.array([2.0, 0.5, 0.1, 0.0]), + "per_run_cont_upper": np.array([15.0, 5.0, 3.0, 0.5]), + "shared_cont_lower": np.array([0.0, 0.0, 0.5]), + "shared_cont_upper": np.array([5.0, 5.0, 5.0]), + "matrix_map_int": [PROTEIN_PFASUM43, PROTEIN_PFASUM60, PROTEIN], + "matrix_map_str": ["pfasum43", "pfasum60", "protein"], + "matrix_names": {PROTEIN_PFASUM43: "P43", PROTEIN_PFASUM60: "P60", PROTEIN: "CB66"}, + "n_matrices": 3, + "seq_type_int": PROTEIN, # C constant for ensemble seq_type + "seq_type_str": "protein", # Python API string + "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range + }, + "rna": { + # RNA gap penalties tend to be different — wider search range for gpo + "per_run_cont_lower": np.array([1.0, 0.2, 0.05, 0.0]), + "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), + # vsm_amax: 0 by default for RNA, but let optimizer explore [0, 3] + # seq_weights: 0 by default for RNA, search [0, 3] + "shared_cont_lower": np.array([0.0, 0.0, 0.5]), + "shared_cont_upper": np.array([3.0, 3.0, 5.0]), + # RNA has only one scoring type + "matrix_map_int": [RNA], + "matrix_map_str": ["rna"], + "matrix_names": {RNA: "RNA"}, + "n_matrices": 1, + "seq_type_int": RNA, + "seq_type_str": "rna", + "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range + }, + "dna": { + "per_run_cont_lower": np.array([1.0, 0.2, 0.05, 0.0]), + "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), + "shared_cont_lower": np.array([0.0, 0.0, 0.5]), + "shared_cont_upper": np.array([3.0, 3.0, 5.0]), + "matrix_map_int": [DNA], + "matrix_map_str": ["dna"], + "matrix_names": {DNA: "DNA"}, + "n_matrices": 1, + "seq_type_int": DNA, + "seq_type_str": "dna", + "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range + }, +} + +# Active profile (set by main() based on --dataset) +_active_profile = PARAM_PROFILES["protein"] + +def set_active_profile(profile_name: str): + global _active_profile + _active_profile = PARAM_PROFILES[profile_name] + +# Convenience accessors for the active profile +def _matrix_map_int(): return _active_profile["matrix_map_int"] +def _matrix_map_str(): return _active_profile["matrix_map_str"] +def _matrix_names(): return _active_profile["matrix_names"] + +# Legacy aliases for backward compat (used in view_pareto.py etc.) +MATRIX_MAP_INT = PARAM_PROFILES["protein"]["matrix_map_int"] +MATRIX_MAP_STR = PARAM_PROFILES["protein"]["matrix_map_str"] +MATRIX_NAMES = PARAM_PROFILES["protein"]["matrix_names"] + + +def get_vars(max_runs: int) -> dict: + """Return pymoo mixed-variable space definition.""" + profile = _active_profile + lo = profile["per_run_cont_lower"] + hi = profile["per_run_cont_upper"] + slo = profile["shared_cont_lower"] + shi = profile["shared_cont_upper"] + + variables = {} + for k in range(max_runs): + variables[f"gpo_{k}"] = Real(bounds=(float(lo[0]), float(hi[0]))) + variables[f"gpe_{k}"] = Real(bounds=(float(lo[1]), float(hi[1]))) + variables[f"tgpe_{k}"] = Real(bounds=(float(lo[2]), float(hi[2]))) + variables[f"noise_{k}"] = Real(bounds=(float(lo[3]), float(hi[3]))) + variables[f"matrix_{k}"] = Choice(options=list(range(profile["n_matrices"]))) + + variables["vsm_amax"] = Real(bounds=(float(slo[0]), float(shi[0]))) + variables["seq_weights"] = Real(bounds=(float(slo[1]), float(shi[1]))) + variables["consistency_weight"] = Real(bounds=(float(slo[2]), float(shi[2]))) + + consistency_options = CONSISTENCY_MAP[:profile.get("max_consistency_idx", len(CONSISTENCY_MAP) - 1) + 1] + variables["n_runs"] = Choice(options=N_RUNS_MAP) + variables["consistency"] = Choice(options=consistency_options) + variables["realign"] = Integer(bounds=(0, 2)) + variables["refine"] = Choice(options=REFINE_MAP) + variables["min_support"] = Integer(bounds=(0, max_runs)) + + return variables + + +def decode_unified_params(x, max_runs: int): + """Decode a mixed-variable dict into a unified parameter dict. + + x is a dict with keys like 'gpo_0', 'n_runs', 'consistency', etc. + Values are native types (float for Real, int for Integer/Choice). + """ + n_runs = int(x["n_runs"]) + consistency = int(x["consistency"]) + realign = int(x["realign"]) + refine = int(x["refine"]) + min_support_raw = int(x["min_support"]) + + vsm_amax = float(x["vsm_amax"]) + seq_weights = float(x["seq_weights"]) + consistency_weight = float(x["consistency_weight"]) + + run_gpo, run_gpe, run_tgpe, run_noise = [], [], [], [] + run_types, run_matrices = [], [] + + for k in range(n_runs): + run_gpo.append(float(x[f"gpo_{k}"])) + run_gpe.append(float(x[f"gpe_{k}"])) + run_tgpe.append(float(x[f"tgpe_{k}"])) + run_noise.append(float(x[f"noise_{k}"])) + matrix_idx = int(x[f"matrix_{k}"]) + run_types.append(_matrix_map_int()[matrix_idx]) + run_matrices.append(_matrix_map_str()[matrix_idx]) + + # --- Masking rules --- + + # Single-run: no tree noise (deterministic tree), no min_support + if n_runs == 1: + run_noise = [0.0] + min_support = 0 + else: + min_support = min(min_support_raw, n_runs) + + # When realign > 0, noise is ineffective (alignment-derived tree) + if realign > 0: + run_noise = [0.0] * n_runs + + # When consistency == 0, consistency_weight is irrelevant + if consistency == 0: + consistency_weight = 1.0 + + return { + "n_runs": n_runs, + "run_gpo": run_gpo, + "run_gpe": run_gpe, + "run_tgpe": run_tgpe, + "run_noise": run_noise, + "run_types": run_types, + "run_matrices": run_matrices, + "vsm_amax": vsm_amax, + "seq_weights": seq_weights, + "consistency_weight": consistency_weight, + "consistency": consistency, + "realign": realign, + "refine": refine, + "min_support": min_support, + } + + +def encode_unified_params(params, max_runs: int) -> dict: + """Encode a unified params dict into a mixed-variable dict.""" + x: dict = {} + for k in range(max_runs): + if k < params["n_runs"]: + x[f"gpo_{k}"] = params["run_gpo"][k] + x[f"gpe_{k}"] = params["run_gpe"][k] + x[f"tgpe_{k}"] = params["run_tgpe"][k] + x[f"noise_{k}"] = params["run_noise"][k] + x[f"matrix_{k}"] = _matrix_map_int().index(params["run_types"][k]) + else: + x[f"gpo_{k}"] = params["run_gpo"][0] + x[f"gpe_{k}"] = params["run_gpe"][0] + x[f"tgpe_{k}"] = params["run_tgpe"][0] + x[f"noise_{k}"] = params["run_noise"][0] + x[f"matrix_{k}"] = _matrix_map_int().index(params["run_types"][0]) + + x["vsm_amax"] = params["vsm_amax"] + x["seq_weights"] = params["seq_weights"] + x["consistency_weight"] = params["consistency_weight"] + x["n_runs"] = params["n_runs"] + x["consistency"] = params["consistency"] + x["realign"] = params["realign"] + x["refine"] = params["refine"] + x["min_support"] = params["min_support"] + return x + + +def mode_label(params): + """Short mode label: 'single', 'ens3', 'ens5'.""" + n = params["n_runs"] + return "single" if n == 1 else f"ens{n}" + + +def format_unified_short(params): + """Compact one-line summary.""" + n_runs = params["n_runs"] + ref = REFINE_NAMES.get(params["refine"], "?") + + if n_runs == 1: + mat = _matrix_names().get(params["run_types"][0], "?") + return (f"{mode_label(params)} {mat} gpo={params['run_gpo'][0]:.1f} " + f"vsm={params['vsm_amax']:.1f} sw={params['seq_weights']:.1f} " + f"c={params['consistency']} re={params['realign']} ref={ref}") + else: + return (f"{mode_label(params)} vsm={params['vsm_amax']:.1f} " + f"sw={params['seq_weights']:.1f} c={params['consistency']} " + f"re={params['realign']} ref={ref} ms={params['min_support']}") + + +def format_unified_long(params): + """Verbose multi-line summary.""" + lines = [f"mode={mode_label(params)} n_runs={params['n_runs']}"] + for k in range(params["n_runs"]): + mat = _matrix_names().get(params["run_types"][k], "?") + lines.append(f" run_{k}: gpo={params['run_gpo'][k]:.3f} " + f"gpe={params['run_gpe'][k]:.3f} " + f"tgpe={params['run_tgpe'][k]:.3f} " + f"noise={params['run_noise'][k]:.3f} {mat}") + ref = REFINE_LONG.get(params["refine"], "?") + lines.append(f" vsm_amax={params['vsm_amax']:.3f} " + f"seq_weights={params['seq_weights']:.3f}") + lines.append(f" consistency={params['consistency']} " + f"consistency_weight={params['consistency_weight']:.3f}") + lines.append(f" realign={params['realign']} refine={ref} " + f"min_support={params['min_support']}") + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Stratified k-fold CV (shared with optimize_params) +# --------------------------------------------------------------------------- + +def stratified_kfold(cases: List[BenchmarkCase], k: int, seed: int = 42 + ) -> List[Tuple[List[BenchmarkCase], List[BenchmarkCase]]]: + """Split cases into k stratified folds by dataset (RV category).""" + rng = np.random.RandomState(seed) + + by_cat: Dict[str, List[BenchmarkCase]] = defaultdict(list) + for c in cases: + by_cat[c.dataset].append(c) + + fold_assignments: Dict[str, int] = {} + for cat_cases in by_cat.values(): + indices = list(range(len(cat_cases))) + rng.shuffle(indices) + for rank, idx in enumerate(indices): + fold_assignments[cat_cases[idx].family] = rank % k + + folds = [] + for fold_idx in range(k): + test = [c for c in cases if fold_assignments[c.family] == fold_idx] + train = [c for c in cases if fold_assignments[c.family] != fold_idx] + folds.append((train, test)) + + return folds + + +# --------------------------------------------------------------------------- +# Evaluation +# --------------------------------------------------------------------------- + +def evaluate_unified(params, cases, n_threads=1, quiet=True): + """Run kalign with unified params on all cases, return mean metrics.""" + results_by_cat: Dict[str, list] = {} + total_time = 0.0 + n_runs = params["n_runs"] + + for case in cases: + with tempfile.TemporaryDirectory() as tmpdir: + output = Path(tmpdir) / f"{case.family}_aln.fasta" + + try: + start = time.perf_counter() + + if n_runs == 1: + # Single-run path + kalign.align_file_to_file( + str(case.unaligned), + str(output), + format="fasta", + seq_type=params["run_matrices"][0], + gap_open=params["run_gpo"][0], + gap_extend=params["run_gpe"][0], + terminal_gap_extend=params["run_tgpe"][0], + n_threads=n_threads, + vsm_amax=params["vsm_amax"], + seq_weights=params["seq_weights"], + consistency=params["consistency"], + consistency_weight=params["consistency_weight"], + realign=params["realign"], + refine=params["refine"], + mode=None, + ) + else: + # Ensemble path + ensemble_custom_file_to_file( + str(case.unaligned), + str(output), + run_gpo=params["run_gpo"], + run_gpe=params["run_gpe"], + run_tgpe=params["run_tgpe"], + run_noise=params["run_noise"], + run_types=params["run_types"], + format="fasta", + seq_type=_active_profile["seq_type_int"], + seed=42, + min_support=params["min_support"], + refine=params["refine"], + vsm_amax=params["vsm_amax"], + realign=params["realign"], + seq_weights=params["seq_weights"], + n_threads=n_threads, + consistency_anchors=params["consistency"], + consistency_weight=params["consistency_weight"], + ) + + wall_time = time.perf_counter() - start + total_time += wall_time + + detailed = score_alignment_detailed(case.reference, output) + + cat = case.dataset + if cat not in results_by_cat: + results_by_cat[cat] = [] + results_by_cat[cat].append(detailed) + + except Exception as e: + if not quiet: + print(f" WARN: {case.family}: {e}", file=sys.stderr) + + if not results_by_cat: + return {"f1": 0.0, "tc": 0.0, "recall": 0.0, "precision": 0.0, + "wall_time": total_time, "per_category": {}} + + per_cat = {} + for cat, scores in results_by_cat.items(): + per_cat[cat] = { + "f1": np.mean([s["f1"] for s in scores]), + "tc": np.mean([s["tc"] for s in scores]), + "recall": np.mean([s["recall"] for s in scores]), + "precision": np.mean([s["precision"] for s in scores]), + "n": len(scores), + } + + all_f1 = [v["f1"] for v in per_cat.values()] + all_tc = [v["tc"] for v in per_cat.values()] + all_recall = [v["recall"] for v in per_cat.values()] + all_precision = [v["precision"] for v in per_cat.values()] + + return { + "f1": float(np.mean(all_f1)), + "tc": float(np.mean(all_tc)), + "recall": float(np.mean(all_recall)), + "precision": float(np.mean(all_precision)), + "wall_time": total_time, + "per_category": per_cat, + } + + +def evaluate_cv(params, folds, n_threads=1, quiet=True): + """Evaluate unified params using stratified k-fold CV.""" + fold_f1s = [] + fold_tcs = [] + total_time = 0.0 + + for _, test in folds: + result = evaluate_unified(params, test, n_threads, quiet) + fold_f1s.append(result["f1"]) + fold_tcs.append(result["tc"]) + total_time += result["wall_time"] + + return { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, + "fold_tcs": fold_tcs, + "wall_time": total_time, + } + + +# --------------------------------------------------------------------------- +# Rich live dashboard +# --------------------------------------------------------------------------- + +class Dashboard: + """Rich-based live terminal dashboard for unified optimization.""" + + def __init__(self, n_gen: int, pop_size: int, + baselines: Dict[str, dict], + max_runs: int): + self.n_gen = n_gen + self.pop_size = pop_size + self.baselines = baselines # {"fast": {...}, "accurate": {...}, "ensemble": {...}} + self.max_runs = max_runs + self.console = Console() + + # State + self.current_gen = 0 + self.eval_count = 0 + self.total_evals = n_gen * pop_size + self.gen_start_time = time.time() + self.run_start_time = time.time() + self.current_eval_idx = 0 + + # Best-ever tracking + self.best_f1 = 0.0 + self.best_f1_entry: Optional[dict] = None + self.best_tc = 0.0 + self.best_tc_entry: Optional[dict] = None + self.fastest = float("inf") + self.fastest_entry: Optional[dict] = None + + # Pareto front (updated per generation) + self.pareto_front: List[dict] = [] + + # Generation history + self.gen_history: List[dict] = [] + + # Recent evaluations (ring buffer) + self.recent_evals: List[dict] = [] + + # Progress bar + self.progress = Progress( + SpinnerColumn(), + TextColumn("[bold blue]{task.description}"), + BarColumn(), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TimeElapsedColumn(), + ) + self.gen_task = self.progress.add_task("Generations", total=n_gen) + self.eval_task = self.progress.add_task("Evaluations", total=pop_size) + + self.live = Live(self._build_layout(), console=self.console, refresh_per_second=2) + + def start(self): + self.run_start_time = time.time() + self.live.start() + + def stop(self): + self.live.stop() + + def _build_baselines_panel(self): + lines = [] + for name, bl in self.baselines.items(): + lines.append(f" {name:10s} F1={bl['f1']:.4f} TC={bl['tc']:.4f} " + f"Time={bl['wall_time']:.0f}s") + return Panel("\n".join(lines) if lines else "(computing...)", title="Baselines") + + def _build_status_panel(self): + elapsed = time.time() - self.run_start_time + elapsed_h = elapsed / 3600 + if self.eval_count > 0: + rate = elapsed / self.eval_count + remaining = (self.total_evals - self.eval_count) * rate + eta_h = remaining / 3600 + else: + rate = 0 + eta_h = 0 + + gen_elapsed = time.time() - self.gen_start_time + + text = (f"Gen {self.current_gen}/{self.n_gen} " + f"Eval {self.eval_count}/{self.total_evals}\n" + f"Elapsed {elapsed_h:.1f}h ETA {eta_h:.1f}h\n" + f"Gen time {gen_elapsed:.0f}s Rate {rate:.1f}s/eval") + return Panel(text, title="Progress") + + def _format_best_entry(self, label, e, delta_str): + """Format a best-of entry with full parameter details.""" + p = e["params"] + ref = REFINE_NAMES.get(p.get("refine", 0), "?") + header = f"{label} {mode_label(p)} {e['wall_time']:.0f}s {delta_str}" + # Per-run gap penalties + run_parts = [] + for k in range(p["n_runs"]): + mat = MATRIX_NAMES.get(p["run_types"][k], "?") + noise_str = f" n={p['run_noise'][k]:.2f}" if p["run_noise"][k] > 0 else "" + run_parts.append(f" R{k}: gpo={p['run_gpo'][k]:.2f} " + f"gpe={p['run_gpe'][k]:.2f} " + f"tgpe={p['run_tgpe'][k]:.2f}{noise_str} {mat}") + shared = (f" vsm={p['vsm_amax']:.2f} sw={p['seq_weights']:.2f} " + f"c={p['consistency']} cw={p['consistency_weight']:.2f} " + f"re={p['realign']} ref={ref} ms={p['min_support']}") + return "\n".join([header] + run_parts + [shared]) + + def _build_best_panel(self): + lines = [] + if self.best_f1_entry: + e = self.best_f1_entry + bl_f1 = max(bl["f1"] for bl in self.baselines.values()) if self.baselines else 0 + delta = e["f1"] - bl_f1 + lines.append(self._format_best_entry( + f"Best F1: {e['f1']:.4f} TC={e['tc']:.4f}", e, + f"({delta:+.4f} vs best baseline)")) + if self.best_tc_entry: + e = self.best_tc_entry + bl_tc = max(bl["tc"] for bl in self.baselines.values()) if self.baselines else 0 + delta = e["tc"] - bl_tc + lines.append(self._format_best_entry( + f"Best TC: {e['tc']:.4f} F1={e['f1']:.4f}", e, + f"({delta:+.4f} vs best baseline)")) + if self.fastest_entry: + e = self.fastest_entry + lines.append(self._format_best_entry( + f"Fastest: {e['wall_time']:.1f}s F1={e['f1']:.4f}", e, "")) + return Panel("\n".join(lines) if lines else "(no data yet)", title="Best by Objective") + + def _build_pareto_table(self): + table = Table(title="Pareto Front (top 12 by F1)", box=None, padding=(0, 1)) + table.add_column("#", style="dim", width=2) + table.add_column("Mode", width=6) + table.add_column("F1", justify="right", width=6) + table.add_column("TC", justify="right", width=6) + table.add_column("Time", justify="right", width=5) + table.add_column("c", width=2) + table.add_column("re", width=2) + table.add_column("ref", width=3) + table.add_column("Params", no_wrap=True) + + sorted_front = sorted(self.pareto_front, key=lambda x: -x["f1"])[:12] + bl_f1 = max(bl["f1"] for bl in self.baselines.values()) if self.baselines else 0 + bl_tc = max(bl["tc"] for bl in self.baselines.values()) if self.baselines else 0 + + for i, entry in enumerate(sorted_front): + p = entry.get("params", {}) + f1_style = "bold green" if entry["f1"] > bl_f1 else "" + tc_style = "bold green" if entry["tc"] > bl_tc else "" + ref = REFINE_NAMES.get(p.get("refine", 0), "?") + + # Build detailed params string + run_strs = [] + for k in range(p.get("n_runs", 1)): + mat = MATRIX_NAMES.get(p["run_types"][k], "?") + run_strs.append(f"R{k}:{p['run_gpo'][k]:.1f}/{p['run_gpe'][k]:.2f}/" + f"{p['run_tgpe'][k]:.2f}/{mat}") + runs = " ".join(run_strs) + shared = (f"vsm={p.get('vsm_amax', 0):.1f} " + f"sw={p.get('seq_weights', 0):.1f} " + f"ms={p.get('min_support', 0)}") + params_str = f"{runs} | {shared}" + + table.add_row( + str(i), + mode_label(p), + Text(f"{entry['f1']:.4f}", style=f1_style), + Text(f"{entry['tc']:.4f}", style=tc_style), + f"{entry.get('wall_time', 0):.0f}s", + str(p.get("consistency", 0)), + str(p.get("realign", 0)), + ref, + params_str, + ) + return table + + def _build_mode_panel(self): + # Count modes in Pareto front and recent + pareto_modes: Dict[str, int] = defaultdict(int) + for entry in self.pareto_front: + pareto_modes[mode_label(entry.get("params", {}))] += 1 + parts = [f"{m}={n}" for m, n in sorted(pareto_modes.items())] + return Panel(f"Pareto: {' '.join(parts)}" if parts else "(none)", + title="Mode Distribution") + + def _build_trend_panel(self): + lines = [] + for h in self.gen_history[-6:]: + lines.append(f"Gen {h['gen']:3d}: F1={h['best_f1']:.4f} " + f"TC={h['best_tc']:.4f} n_pareto={h['n_pareto']}") + return Panel("\n".join(lines) if lines else "(no data)", title="Trend") + + def _build_recent_panel(self): + lines = [] + for e in self.recent_evals[-5:]: + p = e.get("params", {}) + ref = REFINE_NAMES.get(p.get("refine", 0), "?") + mat = MATRIX_NAMES.get(p["run_types"][0], "?") if p.get("run_types") else "?" + gpo = p["run_gpo"][0] if p.get("run_gpo") else 0 + lines.append(f"F1={e['f1']:.4f} TC={e['tc']:.4f} " + f"t={e['wall_time']:.0f}s {mode_label(p)} " + f"gpo={gpo:.1f} {mat} " + f"vsm={p.get('vsm_amax', 0):.1f} " + f"c={p.get('consistency', 0)} re={p.get('realign', 0)} ref={ref}") + return Panel("\n".join(lines) if lines else "(none)", title="Recent") + + def _build_layout(self): + layout = Layout() + layout.split_column( + Layout(name="top", size=6), + Layout(name="baselines", size=5), + Layout(name="best", size=24), + Layout(name="middle", size=16), + Layout(name="bottom", size=8), + ) + layout["top"].split_row( + Layout(self._build_status_panel(), name="status"), + Layout(self.progress, name="progress"), + ) + layout["baselines"].update(self._build_baselines_panel()) + layout["best"].update(self._build_best_panel()) + layout["middle"].split_row( + Layout(self._build_pareto_table(), name="pareto", ratio=3), + Layout(self._build_trend_panel(), name="trend", ratio=2), + ) + layout["bottom"].split_row( + Layout(self._build_mode_panel(), name="modes"), + Layout(self._build_recent_panel(), name="recent", ratio=2), + ) + return layout + + def refresh(self): + self.live.update(self._build_layout()) + + def on_eval_start(self, params: dict, eval_num: int, eval_in_gen: int): + self.eval_count = eval_num + self.current_eval_idx = eval_in_gen + self.progress.update(self.eval_task, completed=eval_in_gen) + self.refresh() + + def on_eval_end(self, params: dict, cv_result: dict): + f1 = cv_result["f1"] + tc = cv_result["tc"] + wt = cv_result.get("wall_time", 0.0) + + entry = {"params": params, "f1": f1, "tc": tc, "wall_time": wt} + self.recent_evals.append(entry) + if len(self.recent_evals) > 5: + self.recent_evals.pop(0) + + if f1 > self.best_f1: + self.best_f1 = f1 + self.best_f1_entry = entry + if tc > self.best_tc: + self.best_tc = tc + self.best_tc_entry = entry + if wt < self.fastest and f1 > 0.5: + self.fastest = wt + self.fastest_entry = entry + + self.refresh() + + def on_gen_start(self, gen: int): + self.current_gen = gen + self.gen_start_time = time.time() + self.progress.update(self.gen_task, completed=gen) + self.progress.update(self.eval_task, completed=0) + self.refresh() + + def on_gen_end(self, gen: int, pareto_front: List[dict]): + self.pareto_front = pareto_front + + best_f1_in_gen = max((s["f1"] for s in pareto_front), default=0.0) + best_tc_in_gen = max((s["tc"] for s in pareto_front), default=0.0) + self.gen_history.append({ + "gen": gen, + "best_f1": best_f1_in_gen, + "best_tc": best_tc_in_gen, + "n_pareto": len(pareto_front), + }) + + self.progress.update(self.gen_task, completed=gen) + self.refresh() + + +# --------------------------------------------------------------------------- +# Parallel evaluation helper (must be top-level for pickling) +# --------------------------------------------------------------------------- + +def _eval_one_unified_fold(args_tuple): + """Evaluate one (individual, fold) pair.""" + ind_idx, fold_idx, x, test_cases, n_threads, max_runs = args_tuple + params = decode_unified_params(x, max_runs) + result = evaluate_unified(params, test_cases, n_threads, quiet=True) + return ind_idx, fold_idx, params, result + + +def _eval_baseline(args_tuple): + """Evaluate one baseline configuration on a set of cases.""" + name, fi, bl_params, test_cases, n_threads = args_tuple + result = evaluate_unified(bl_params, test_cases, n_threads, quiet=True) + return name, fi, result + + +def _kill_pool(pool: ProcessPoolExecutor) -> None: + """Forcefully terminate all worker processes.""" + for pid in list(pool._processes): # noqa: SLF001 + try: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + pool.shutdown(wait=False, cancel_futures=True) + + +# --------------------------------------------------------------------------- +# pymoo Problem + Callback +# --------------------------------------------------------------------------- + +class UnifiedCVProblem(Problem): + """3-objective optimization with stratified CV evaluation.""" + + def __init__(self, folds, max_runs: int, n_threads=1, n_workers=1, + dashboard: Optional[Dashboard] = None): + super().__init__( + vars=get_vars(max_runs), + n_obj=3, # always 3: -F1, -TC, time + ) + self.folds = folds + self.max_runs = max_runs + self.n_threads = n_threads + self.n_workers = n_workers + self.dashboard = dashboard + self.eval_count = 0 + self.history: List[dict] = [] + + def _evaluate(self, X, out, *_args, **_kwargs): + F = np.zeros((len(X), 3)) + + if self.n_workers > 1: + self._evaluate_parallel(X, F) + else: + self._evaluate_serial(X, F) + + out["F"] = F + + def _evaluate_serial(self, X, F): + for i, x in enumerate(X): + params = decode_unified_params(x, self.max_runs) + self.eval_count += 1 + + if self.dashboard: + self.dashboard.on_eval_start(params, self.eval_count, i) + + cv_result = evaluate_cv(params, self.folds, self.n_threads, quiet=True) + self._record(i, F, params, cv_result) + + def _evaluate_parallel(self, X, F): + """Fine-grained parallelism: submit (individual x fold) jobs.""" + n_folds = len(self.folds) + + jobs = [] + for i, x in enumerate(X): + for fold_idx, (_, test) in enumerate(self.folds): + jobs.append((i, fold_idx, x, test, self.n_threads, self.max_runs)) + + if self.dashboard: + self.dashboard.on_eval_start({}, self.eval_count + 1, 0) + + fold_results: Dict[int, Dict[int, dict]] = defaultdict(dict) + ind_params: Dict[int, dict] = {} + + pool = ProcessPoolExecutor(max_workers=self.n_workers) + try: + futures = {pool.submit(_eval_one_unified_fold, j): j[:2] for j in jobs} + completed_individuals = set() + + for future in as_completed(futures): + ind_idx, fold_idx, params, result = future.result() + fold_results[ind_idx][fold_idx] = result + ind_params[ind_idx] = params + + if len(fold_results[ind_idx]) == n_folds: + completed_individuals.add(ind_idx) + self.eval_count += 1 + + fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] + fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] + total_time = sum(fold_results[ind_idx][fi]["wall_time"] + for fi in range(n_folds)) + + cv_result = { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "fold_f1s": fold_f1s, + "fold_tcs": fold_tcs, + "wall_time": total_time, + } + + self._record(ind_idx, F, params, cv_result) + + if self.dashboard: + self.dashboard.on_eval_start( + params, self.eval_count, len(completed_individuals)) + + except KeyboardInterrupt: + _kill_pool(pool) + raise + finally: + pool.shutdown(wait=False) + + def _record(self, i, F, params, cv_result): + F[i, 0] = -cv_result["f1"] + F[i, 1] = -cv_result["tc"] + F[i, 2] = cv_result["wall_time"] + + self.history.append({ + "eval": self.eval_count, + "params": params, + "cv_result": cv_result, + }) + + if self.dashboard: + self.dashboard.on_eval_end(params, cv_result) + + +class GenerationCallback(Callback): + """pymoo callback: updates dashboard + saves checkpoint after each generation.""" + + def __init__(self, dashboard: Optional[Dashboard] = None, + checkpoint_path: Optional[Path] = None, + problem: Optional["UnifiedCVProblem"] = None, + max_runs: int = 5): + super().__init__() + self.dashboard = dashboard + self.checkpoint_path = checkpoint_path + self.problem = problem + self.max_runs = max_runs + + def notify(self, algorithm): + gen = algorithm.n_gen + + if self.dashboard: + self.dashboard.on_gen_start(gen) + + # Extract Pareto front + pareto = [] + if algorithm.opt is not None and len(algorithm.opt) > 0: + for ind in algorithm.opt: + params = decode_unified_params(ind.X, self.max_runs) + entry = { + "params": params, + "f1": -ind.F[0], + "tc": -ind.F[1], + "wall_time": ind.F[2], + } + pareto.append(entry) + + if self.dashboard: + self.dashboard.on_gen_end(gen, pareto) + + # Save checkpoint + if self.checkpoint_path: + pop = algorithm.pop + # Deep-copy dicts in X (mixed-variable: array of dicts) + raw_X = pop.get("X") + pop_X = np.array([dict(d) for d in raw_X], dtype=object) + ckpt = { + "format": "mixed_v1", + "pop_X": pop_X, + "pop_F": pop.get("F").copy(), + "pop_G": pop.get("G"), + "pop_H": pop.get("H"), + "n_gen_completed": gen, + "history": self.problem.history if self.problem else [], + "pop_size": len(pop), + "max_runs": self.max_runs, + "profile": _active_profile.get("seq_type_str", "protein"), + } + tmp = self.checkpoint_path.with_suffix(".tmp") + with open(tmp, "wb") as f: + pickle.dump(ckpt, f) + tmp.rename(self.checkpoint_path) + + +def load_checkpoint(path: Path): + """Load a generation checkpoint.""" + with open(path, "rb") as f: + ckpt = pickle.load(f) # noqa: S301 + return ckpt + + +# --------------------------------------------------------------------------- +# Baseline definitions +# --------------------------------------------------------------------------- + +BASELINE_CONFIGS_PROTEIN = { + "fast": { + "n_runs": 1, + "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], + "run_types": [PROTEIN_PFASUM60], "run_matrices": ["pfasum60"], + "vsm_amax": 2.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 0, "refine": REFINE_NONE, "min_support": 0, + }, + "accurate": { + "n_runs": 1, + "run_gpo": [8.472], "run_gpe": [0.554], "run_tgpe": [0.409], "run_noise": [0.0], + "run_types": [PROTEIN_PFASUM60], "run_matrices": ["pfasum60"], + "vsm_amax": 1.359, "seq_weights": 3.407, "consistency_weight": 1.167, + "consistency": 8, "realign": 2, "refine": REFINE_NONE, "min_support": 0, + }, + "ensemble": { + "n_runs": 3, + "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], + "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], + "run_types": [PROTEIN_PFASUM60, PROTEIN_PFASUM60, PROTEIN_PFASUM60], + "run_matrices": ["pfasum60", "pfasum60", "pfasum60"], + "vsm_amax": 2.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 1, "refine": REFINE_CONFIDENT, "min_support": 0, + }, +} + +BASELINE_CONFIGS_RNA = { + "fast": { + "n_runs": 1, + "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], + "run_types": [RNA], "run_matrices": ["rna"], + "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 0, "refine": REFINE_NONE, "min_support": 0, + }, + "accurate": { + "n_runs": 1, + "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], + "run_types": [RNA], "run_matrices": ["rna"], + "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 8, "realign": 2, "refine": REFINE_NONE, "min_support": 0, + }, + "ensemble": { + "n_runs": 3, + "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], + "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], + "run_types": [RNA, RNA, RNA], "run_matrices": ["rna", "rna", "rna"], + "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 1, "refine": REFINE_CONFIDENT, "min_support": 0, + }, +} + +BASELINE_CONFIGS_DNA = { + "fast": { + "n_runs": 1, + "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], + "run_types": [DNA], "run_matrices": ["dna"], + "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 0, "refine": REFINE_NONE, "min_support": 0, + }, + "accurate": { + "n_runs": 1, + "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], + "run_types": [DNA], "run_matrices": ["dna"], + "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 8, "realign": 2, "refine": REFINE_NONE, "min_support": 0, + }, + "ensemble": { + "n_runs": 3, + "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], + "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], + "run_types": [DNA, DNA, DNA], "run_matrices": ["dna", "dna", "dna"], + "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 8, "realign": 1, "refine": REFINE_CONFIDENT, "min_support": 0, + }, +} + +def get_baseline_configs(dataset: str) -> dict: + """Return baseline configs appropriate for the dataset.""" + if dataset == "bralibase": + return BASELINE_CONFIGS_RNA + if dataset == "mdsa": + return BASELINE_CONFIGS_DNA + return BASELINE_CONFIGS_PROTEIN + +# Default for backward compat +BASELINE_CONFIGS = BASELINE_CONFIGS_PROTEIN + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Unified kalign hyperparameter optimization with NSGA-II (3 objectives)") + parser.add_argument("--max-runs", type=int, default=5, + help="Max ensemble runs. n_runs choices: {1,3,5} (default: 5)") + parser.add_argument("--pop-size", type=int, default=200, + help="Population size (default: 200)") + parser.add_argument("--n-gen", type=int, default=100, + help="Number of generations (default: 100)") + parser.add_argument("--n-folds", type=int, default=5, + help="Number of CV folds (default: 5)") + parser.add_argument("--n-threads", type=int, default=1, + help="OpenMP threads per kalign alignment (default: 1)") + parser.add_argument("--n-workers", type=int, default=1, + help="Parallel worker processes (default: 1)") + parser.add_argument("--seed", type=int, default=42, + help="Random seed (default: 42)") + parser.add_argument("--output-dir", type=str, default="benchmarks/results/unified_optim", + help="Output directory for results") + parser.add_argument("--run-name", type=str, default=None, + help="Name for this run (creates subdirectory)") + parser.add_argument("--no-dashboard", action="store_true", + help="Disable rich dashboard, use plain text output") + parser.add_argument("--dataset", type=str, default="balibase", + choices=["balibase", "bralibase", "mdsa"], + help="Benchmark dataset (default: balibase)") + parser.add_argument("--resume", type=str, default=None, + help="Resume from a generation checkpoint file (.pkl)") + args = parser.parse_args() + + console = Console() + + # --- Dataset setup --- + if args.dataset == "balibase": + set_active_profile("protein") + if not balibase_is_available(): + console.print("Downloading BAliBASE...") + balibase_download() + cases = balibase_cases() + elif args.dataset == "bralibase": + set_active_profile("rna") + if not bralibase_is_available(): + console.print("Downloading BRAliBASE...") + bralibase_download() + cases = bralibase_cases() + elif args.dataset == "mdsa": + set_active_profile("dna") + if not mdsa_is_available(): + console.print("Downloading MDSA...") + mdsa_download() + cases = mdsa_cases() + else: + console.print(f"[bold red]Unknown dataset: {args.dataset}[/]") + return + + console.print(f"Loaded [bold]{len(cases)}[/] {args.dataset} cases " + f"(profile: {_active_profile['seq_type_str']})") + + cats: Dict[str, int] = {} + for c in cases: + cats[c.dataset] = cats.get(c.dataset, 0) + 1 + for cat, n in sorted(cats.items()): + console.print(f" {cat}: {n} cases") + + # Build stratified folds + k = args.n_folds + folds = stratified_kfold(cases, k, seed=args.seed) + console.print(f"\nStratified [bold]{k}[/]-fold CV:") + for i, (train, test) in enumerate(folds): + test_cats: Dict[str, int] = defaultdict(int) + for c in test: + test_cats[c.dataset] += 1 + cat_str = ", ".join(f"{cat.replace('balibase_', '')}:{n}" + for cat, n in sorted(test_cats.items())) + console.print(f" Fold {i}: {len(test)} test / {len(train)} train ({cat_str})") + + output_dir = Path(args.output_dir) + if args.run_name: + output_dir = output_dir / args.run_name + else: + output_dir = output_dir / args.dataset + output_dir.mkdir(parents=True, exist_ok=True) + + max_runs = args.max_runs + n_var = len(get_vars(max_runs)) + console.print(f"\nDecision vector: {n_var} variables (max_runs={max_runs})") + + # --- Baseline evaluations (parallelized) --- + bl_configs = get_baseline_configs(args.dataset) + console.print(f"\n[bold]Baseline evaluations[/] ({k}-fold CV)") + n_baseline_workers = max(1, args.n_workers) + baselines: Dict[str, dict] = {} + + # Build all baseline jobs: (name, fold_idx, cases) + baseline_jobs = [] + for name, bl_params in bl_configs.items(): + for fi, (_, test) in enumerate(folds): + baseline_jobs.append((name, fi, bl_params, test, args.n_threads)) + # Full-dataset as extra fold + baseline_jobs.append((name, k, bl_params, cases, args.n_threads)) + + if n_baseline_workers > 1: + console.print(f" Running {len(baseline_jobs)} baseline jobs in parallel " + f"({n_baseline_workers} workers)...") + bl_fold_results: Dict[str, Dict[int, dict]] = defaultdict(dict) + with ProcessPoolExecutor(max_workers=min(n_baseline_workers, len(baseline_jobs))) as pool: + futures = {pool.submit(_eval_baseline, j): j[:2] for j in baseline_jobs} + for future in as_completed(futures): + name, fi, result = future.result() + bl_fold_results[name][fi] = result + else: + bl_fold_results = defaultdict(dict) + for j in baseline_jobs: + name, fi, result = _eval_baseline(j) + bl_fold_results[name][fi] = result + + for name in bl_configs: + fold_f1s = [bl_fold_results[name][fi]["f1"] for fi in range(k)] + fold_tcs = [bl_fold_results[name][fi]["tc"] for fi in range(k)] + total_time = sum(bl_fold_results[name][fi]["wall_time"] for fi in range(k)) + baselines[name] = { + "f1": float(np.mean(fold_f1s)), + "tc": float(np.mean(fold_tcs)), + "f1_std": float(np.std(fold_f1s)), + "tc_std": float(np.std(fold_tcs)), + "wall_time": total_time, + } + bl_full = bl_fold_results[name][k] + console.print(f" {name:10s} CV F1={baselines[name]['f1']:.4f}+-{baselines[name]['f1_std']:.4f} " + f"CV TC={baselines[name]['tc']:.4f} " + f"Time={baselines[name]['wall_time']:.0f}s " + f"Full F1={bl_full['f1']:.4f}") + + # --- Optimization --- + n_evals = args.pop_size * args.n_gen + # Use accurate baseline time as estimate + est_sec_per_eval = baselines.get("accurate", {}).get("wall_time", 100) + parallelism = max(1, args.n_workers) + est_hours = n_evals * est_sec_per_eval / parallelism / 3600 + + console.print(f"\n[bold]Starting NSGA-III[/]: pop_size={args.pop_size}, n_gen={args.n_gen}, " + f"{k}-fold CV, {args.n_workers} worker(s) x {args.n_threads} thread(s)") + console.print(f"Total evaluations: ~{n_evals}") + console.print(f"Estimated time: ~{est_hours:.1f} hours " + f"(~{est_sec_per_eval:.0f}s per eval, {parallelism}x parallel)\n") + + # Set up dashboard or plain mode + use_dashboard = not args.no_dashboard + dashboard = None + + if use_dashboard: + dashboard = Dashboard( + n_gen=args.n_gen, + pop_size=args.pop_size, + baselines=baselines, + max_runs=max_runs, + ) + + problem = UnifiedCVProblem( + folds=folds, + max_runs=max_runs, + n_threads=args.n_threads, + n_workers=args.n_workers, + dashboard=dashboard, + ) + + checkpoint_path = output_dir / "gen_checkpoint.pkl" + callback = GenerationCallback( + dashboard=dashboard, + checkpoint_path=checkpoint_path, + problem=problem, + max_runs=max_runs, + ) + + # NSGA-III reference directions for 3 objectives + ref_dirs = get_reference_directions("das-dennis", 3, n_partitions=12) + n_ref = len(ref_dirs) # 91 for n_partitions=12 + mixed_mating = MixedVariableMating( + eliminate_duplicates=MixedVariableDuplicateElimination()) + mixed_dedup = MixedVariableDuplicateElimination() + + # Resume from checkpoint or start fresh + resumed_gen = 0 + if args.resume: + resume_path = Path(args.resume) + if not resume_path.exists(): + console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") + return + ckpt = load_checkpoint(resume_path) + if ckpt.get("format") != "mixed_v1": + console.print("[bold red]Cannot resume from old-format checkpoint.[/]") + console.print("Old checkpoints used float arrays; new format uses mixed variables.") + console.print("Please start a fresh optimization run (remove --resume).") + return + if ckpt.get("max_runs") != max_runs: + console.print(f"[bold red]max_runs mismatch:[/] checkpoint has " + f"{ckpt.get('max_runs')}, requested {max_runs}") + return + ckpt_profile = ckpt.get("profile", "protein") + if ckpt_profile != _active_profile["seq_type_str"]: + console.print(f"[bold red]Profile mismatch:[/] checkpoint has " + f"'{ckpt_profile}', current dataset uses " + f"'{_active_profile['seq_type_str']}'") + return + pop_X = ckpt["pop_X"] + pop_F = ckpt["pop_F"] + pop_G = ckpt.get("pop_G") + pop_H = ckpt.get("pop_H") + resumed_gen = ckpt["n_gen_completed"] + problem.history = ckpt.get("history", []) + problem.eval_count = len(problem.history) + console.print(f"[bold green]Resumed[/] from generation {resumed_gen} " + f"({len(problem.history)} prior evaluations)") + remaining = args.n_gen - resumed_gen + if remaining <= 0: + console.print(f"[bold yellow]Already completed {resumed_gen} generations " + f"(requested {args.n_gen}). Increase --n-gen to continue.[/]") + return + termination = get_termination("n_gen", remaining) + # Reconstruct evaluated population so pymoo skips re-evaluation + pop = Population.new("X", pop_X) + pop.set("F", pop_F) + if pop_G is not None: + pop.set("G", pop_G) + if pop_H is not None: + pop.set("H", pop_H) + for ind in pop: + ind.evaluated = {"F", "G", "H"} + algorithm = NSGA3( + ref_dirs=ref_dirs, + pop_size=len(pop_X), + sampling=pop, + mating=mixed_mating, + eliminate_duplicates=mixed_dedup, + ) + else: + pop_size = args.pop_size + if pop_size < n_ref: + console.print(f"[bold yellow]Warning:[/] pop_size ({pop_size}) < reference " + f"directions ({n_ref}). Consider --pop-size {n_ref} or larger.") + algorithm = NSGA3( + ref_dirs=ref_dirs, + pop_size=pop_size, + sampling=MixedVariableSampling(), + mating=mixed_mating, + eliminate_duplicates=mixed_dedup, + ) + termination = get_termination("n_gen", args.n_gen) + + if dashboard: + dashboard.start() + + try: + res = minimize( + problem, + algorithm, + termination, + seed=args.seed, + verbose=not use_dashboard, + callback=callback, + ) + except KeyboardInterrupt: + if dashboard: + dashboard.stop() + console.print("\n[bold yellow]Interrupted![/] Checkpoint was saved after last " + f"completed generation to:\n {checkpoint_path}") + console.print("Resume with: [bold]--resume " + str(checkpoint_path) + "[/]") + os._exit(1) # noqa: SLF001 + finally: + if dashboard: + dashboard.stop() + + # --- Results --- + console.print(f"\n[bold]Optimization complete.[/] " + f"{len(res.F)} Pareto-optimal solutions found.\n") + + pareto_configs = [] + for i, (x, f) in enumerate(zip(res.X, res.F)): + params = decode_unified_params(x, max_runs) + f1 = -f[0] + tc = -f[1] + wt = f[2] + pareto_configs.append({"params": params, "f1_cv": f1, "tc_cv": tc, "wall_time": wt}) + + # Print Pareto front + table = Table(title="Pareto Front (sorted by CV F1)") + table.add_column("#", style="dim", width=3) + table.add_column("Mode", width=6) + table.add_column("CV F1", justify="right") + table.add_column("CV TC", justify="right") + table.add_column("Time", justify="right") + table.add_column("Parameters") + + sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) + bl_best_f1 = max(bl["f1"] for bl in baselines.values()) + bl_best_tc = max(bl["tc"] for bl in baselines.values()) + + for i, cfg in enumerate(sorted_pareto[:30]): + f1_style = "bold green" if cfg["f1_cv"] > bl_best_f1 else "" + tc_style = "bold green" if cfg["tc_cv"] > bl_best_tc else "" + table.add_row( + str(i), + mode_label(cfg["params"]), + Text(f"{cfg['f1_cv']:.4f}", style=f1_style), + Text(f"{cfg['tc_cv']:.4f}", style=tc_style), + f"{cfg['wall_time']:.0f}s", + format_unified_short(cfg["params"]), + ) + console.print(table) + + # --- Mode summary --- + mode_counts: Dict[str, int] = defaultdict(int) + mode_best_f1: Dict[str, float] = defaultdict(float) + for cfg in pareto_configs: + m = mode_label(cfg["params"]) + mode_counts[m] += 1 + mode_best_f1[m] = max(mode_best_f1[m], cfg["f1_cv"]) + + console.print("\n[bold]Mode distribution on Pareto front:[/]") + for m in sorted(mode_counts.keys()): + console.print(f" {m}: {mode_counts[m]} solutions, best F1={mode_best_f1[m]:.4f}") + + # --- Re-evaluate top-3 on FULL dataset --- + console.print(f"\n[bold]Full-dataset evaluation[/] (top 3 by F1, checking for overfit)") + top3 = sorted_pareto[:3] + for i, cfg in enumerate(top3): + full_result = evaluate_unified(cfg["params"], cases, args.n_threads) + console.print(f"\n [{i}] {mode_label(cfg['params'])} " + f"CV F1={cfg['f1_cv']:.4f} -> Full F1={full_result['f1']:.4f} " + f"CV TC={cfg['tc_cv']:.4f} -> Full TC={full_result['tc']:.4f}") + gap_f1 = full_result["f1"] - cfg["f1_cv"] + gap_tc = full_result["tc"] - cfg["tc_cv"] + console.print(f" Overfit check: F1 {gap_f1:+.4f} TC {gap_tc:+.4f}") + for cat, v in sorted(full_result["per_category"].items()): + console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") + console.print(f" {format_unified_long(cfg['params'])}") + + # --- Recommended tiers --- + console.print(f"\n{'='*60}") + console.print("[bold]Recommended configurations:[/]") + + # Fast: best F1 among solutions under 15s + fast_candidates = [c for c in sorted_pareto if c["wall_time"] < 15] + if fast_candidates: + best_fast = fast_candidates[0] + console.print(f"\n [bold]Fast[/] (< 15s): F1={best_fast['f1_cv']:.4f} " + f"TC={best_fast['tc_cv']:.4f} Time={best_fast['wall_time']:.0f}s") + console.print(f" {format_unified_short(best_fast['params'])}") + + # Default: best F1 among solutions under 60s + default_candidates = [c for c in sorted_pareto if c["wall_time"] < 60] + if default_candidates: + best_default = default_candidates[0] + console.print(f"\n [bold]Default[/] (< 60s): F1={best_default['f1_cv']:.4f} " + f"TC={best_default['tc_cv']:.4f} Time={best_default['wall_time']:.0f}s") + console.print(f" {format_unified_short(best_default['params'])}") + + # Accurate: best F1 overall + best_overall = sorted_pareto[0] + console.print(f"\n [bold]Accurate[/] (best F1): F1={best_overall['f1_cv']:.4f} " + f"TC={best_overall['tc_cv']:.4f} Time={best_overall['wall_time']:.0f}s") + console.print(f" {format_unified_short(best_overall['params'])}") + + # --- Save --- + results = { + "pareto_configs": pareto_configs, + "history": problem.history, + "baselines": baselines, + "folds_info": [(len(tr), len(te)) for tr, te in folds], + "args": vars(args), + "max_runs": max_runs, + "profile": _active_profile["seq_type_str"], + "dataset": args.dataset, + } + results_path = output_dir / "optim_results.pkl" + with open(results_path, "wb") as f: + pickle.dump(results, f) + console.print(f"\nResults saved to {results_path}") + + summary_path = output_dir / "pareto_front.txt" + with open(summary_path, "w") as f: + f.write(f"# Unified kalign optimization (NSGA-II, 3 objectives: F1, TC, time)\n") + f.write(f"# pop_size={args.pop_size} n_gen={args.n_gen} max_runs={max_runs} " + f"n_folds={k} seed={args.seed}\n") + f.write(f"# Baselines:\n") + for name, bl in baselines.items(): + f.write(f"# {name:10s} F1={bl['f1']:.4f} TC={bl['tc']:.4f} " + f"Time={bl['wall_time']:.0f}s\n") + f.write(f"\n") + + for i, cfg in enumerate(sorted_pareto): + p = cfg["params"] + f.write(f"[{i}] mode={mode_label(p)} CV_F1={cfg['f1_cv']:.4f} " + f"CV_TC={cfg['tc_cv']:.4f} Time={cfg['wall_time']:.0f}s\n") + f.write(f" n_runs={p['n_runs']}\n") + for run_k in range(p["n_runs"]): + mat = MATRIX_NAMES.get(p["run_types"][run_k], "?") + f.write(f" run_{run_k}: gpo={p['run_gpo'][run_k]:.3f} " + f"gpe={p['run_gpe'][run_k]:.3f} " + f"tgpe={p['run_tgpe'][run_k]:.3f} " + f"noise={p['run_noise'][run_k]:.3f} {mat}\n") + ref = REFINE_LONG.get(p["refine"], "?") + f.write(f" vsm_amax={p['vsm_amax']:.3f} " + f"seq_weights={p['seq_weights']:.3f}\n") + f.write(f" consistency={p['consistency']} " + f"consistency_weight={p['consistency_weight']:.3f}\n") + f.write(f" realign={p['realign']} refine={ref} " + f"min_support={p['min_support']}\n\n") + + console.print(f"Pareto front saved to {summary_path}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/scoring.py b/benchmarks/scoring.py index 63c69a3..62a4084 100644 --- a/benchmarks/scoring.py +++ b/benchmarks/scoring.py @@ -179,7 +179,7 @@ def score_alignment_detailed(reference: Path, test_output: Path) -> dict: return kalign.compare_detailed(str(reference), str(test_output), column_mask=mask) # For non-BAliBASE references (FASTA format), check for gapless edge case - if reference.suffix in ('.fa', '.fasta') and not _fasta_ref_has_gaps(reference): + if reference.suffix in ('.fa', '.fasta', '.afa') and not _fasta_ref_has_gaps(reference): raise RuntimeError("Reference alignment has no gaps — skipping (trivially aligned)") return kalign.compare_detailed(str(reference), str(test_output), max_gap_frac=-1.0) diff --git a/benchmarks/view_pareto.py b/benchmarks/view_pareto.py new file mode 100644 index 0000000..5386f14 --- /dev/null +++ b/benchmarks/view_pareto.py @@ -0,0 +1,999 @@ +#!/usr/bin/env python3 +"""Interactive Dash app to visualize the Pareto front from a unified optimizer checkpoint. + +Usage: + # View local checkpoint + uv run python -m benchmarks.view_pareto benchmarks/results/unified_optim/gen_checkpoint.pkl + + # Pull from server and view (auto-refresh every 30s) + uv run python -m benchmarks.view_pareto --remote tki-workstation:tmp/kalign35/benchmarks/results/unified_optim/gen_checkpoint.pkl + + # Custom port and refresh interval + uv run python -m benchmarks.view_pareto --port 8051 --refresh 60 checkpoint.pkl +""" + +import argparse +import json +import os +import pickle +import subprocess +import time +from pathlib import Path + +# Disable all Dash/Plotly telemetry before importing +os.environ["DASH_DISABLE_TELEMETRY"] = "1" +os.environ["PLOTLY_RENDERER"] = "browser" + +import numpy as np +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +from dash import Dash, Input, Output, State, ctx, dash_table, dcc, html + +try: + from kneed import KneeLocator +except ImportError: + KneeLocator = None + +from .optimize_unified import ( + MATRIX_NAMES, + REFINE_LONG, + decode_unified_params, + mode_label, +) + + +def load_checkpoint(path: str): + """Load checkpoint, optionally from remote via rsync.""" + with open(path, "rb") as f: + return pickle.load(f) # noqa: S301 + + +def sync_remote(remote_path: str, local_path: str): + """Pull checkpoint from remote server.""" + result = subprocess.run( + ["rsync", "-az", remote_path, local_path], + capture_output=True, text=True, + ) + return result.returncode == 0 + + +def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: + """Build a DataFrame from checkpoint population. + + Returns (DataFrame, params_by_idx) where params_by_idx maps idx -> full decoded params. + Supports both mixed_v1 (dict-per-individual) and legacy (float array) checkpoints. + """ + if ckpt.get("format") != "mixed_v1": + raise ValueError( + "Old-format checkpoint (float arrays). " + "Re-run the optimizer to generate a mixed_v1 checkpoint." + ) + pop_X = ckpt["pop_X"] + pop_F = ckpt["pop_F"] + + rows = [] + params_by_idx = {} + for i in range(len(pop_X)): + params = decode_unified_params(pop_X[i], max_runs) + params_by_idx[i] = params + f1 = -pop_F[i, 0] + tc = -pop_F[i, 1] + wt = pop_F[i, 2] + + ref_name = REFINE_LONG.get(params["refine"], "?") + mode = mode_label(params) + + # Per-run details + run_details = [] + for k in range(params["n_runs"]): + mat = MATRIX_NAMES.get(params["run_types"][k], "?") + noise = params["run_noise"][k] + noise_str = f" n={noise:.2f}" if noise > 0 else "" + run_details.append( + f"R{k}: gpo={params['run_gpo'][k]:.2f} " + f"gpe={params['run_gpe'][k]:.2f} " + f"tgpe={params['run_tgpe'][k]:.2f}{noise_str} {mat}" + ) + + rows.append({ + "idx": i, + "f1": round(f1, 4), + "tc": round(tc, 4), + "wall_time": round(wt, 1), + "mode": mode, + "n_runs": params["n_runs"], + "vsm_amax": round(params["vsm_amax"], 3), + "seq_weights": round(params["seq_weights"], 3), + "consistency": params["consistency"], + "consistency_weight": round(params["consistency_weight"], 3), + "realign": params["realign"], + "refine": ref_name, + "min_support": params["min_support"], + "run_details": "\n".join(run_details), + # Flattened run 0 params for color/hover + "gpo_0": round(params["run_gpo"][0], 2), + "gpe_0": round(params["run_gpe"][0], 2), + "tgpe_0": round(params["run_tgpe"][0], 2), + "matrix_0": MATRIX_NAMES.get(params["run_types"][0], "?"), + }) + + return pd.DataFrame(rows), params_by_idx + + +def build_history_df(ckpt: dict, max_runs: int) -> pd.DataFrame: + """Build DataFrame from evaluation history.""" + history = ckpt.get("history", []) + if not history: + return pd.DataFrame() + + rows = [] + for h in history: + cv = h.get("cv_result", {}) + p = h.get("params", {}) + rows.append({ + "eval": h.get("eval", 0), + "f1": cv.get("f1", 0), + "tc": cv.get("tc", 0), + "wall_time": cv.get("wall_time", 0), + "mode": mode_label(p) if p else "?", + }) + return pd.DataFrame(rows) + + +def _compute_pareto_2d(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: + """Extract the 2D Pareto front from a DataFrame. + + Assumes we want to maximize y_col and minimize x_col (if x is time) + or maximize both (if both are scores). + Returns points sorted by x_col for drawing as a line. + """ + if df.empty: + return df + + # Determine direction: for wall_time we minimize, for f1/tc we maximize + # The Pareto front is the "upper-left" boundary (max y, min x) + # or "upper-right" if both axes are scores to maximize + x_minimize = (x_col == "wall_time") + y_minimize = (y_col == "wall_time") + + points = df[[x_col, y_col]].values + n = len(points) + is_pareto = [True] * n + + for i in range(n): + if not is_pareto[i]: + continue + for j in range(n): + if i == j or not is_pareto[j]: + continue + # Check if j dominates i + xi, yi = points[i] + xj, yj = points[j] + + # "Better" depends on direction + xj_better = (xj < xi) if x_minimize else (xj > xi) + xj_equal = abs(xj - xi) < 1e-10 + yj_better = (yj < yi) if y_minimize else (yj > yi) + yj_equal = abs(yj - yi) < 1e-10 + + if (xj_better or xj_equal) and (yj_better or yj_equal) and (xj_better or yj_better): + is_pareto[i] = False + break + + pareto = df[is_pareto].copy() + pareto = pareto.sort_values(x_col) + return pareto + + +def _compute_pareto_3d(df: pd.DataFrame) -> pd.DataFrame: + """Extract the 3D Pareto front: maximize F1, maximize TC, minimize wall_time. + + A point is non-dominated if no other point is better in all three objectives. + """ + if df.empty: + return df + + n = len(df) + f1 = df["f1"].values + tc = df["tc"].values + wt = df["wall_time"].values + is_pareto = [True] * n + + for i in range(n): + if not is_pareto[i]: + continue + for j in range(n): + if i == j or not is_pareto[j]: + continue + # j dominates i if: f1_j >= f1_i AND tc_j >= tc_i AND wt_j <= wt_i + # with at least one strict inequality + if (f1[j] >= f1[i] and tc[j] >= tc[i] and wt[j] <= wt[i] and + (f1[j] > f1[i] or tc[j] > tc[i] or wt[j] < wt[i])): + is_pareto[i] = False + break + + return df[is_pareto].copy() + + +def auto_select_tiers(pareto_df: pd.DataFrame) -> dict: + """Auto-select fast/default/accurate tiers from the 2D Pareto front (F1 vs time). + + Uses the kneedle algorithm to find knee points on the F1-vs-time Pareto curve. + The knee point is where spending more time gives diminishing F1 returns. + + Returns dict with keys "fast", "default", "accurate", each containing + the DataFrame row index (idx column) or None if not enough data. + """ + if pareto_df.empty or len(pareto_df) < 3: + return {"fast": None, "default": None, "accurate": None} + + # Get the 2D Pareto front: maximize F1, minimize time + front = _compute_pareto_2d(pareto_df, "wall_time", "f1") + if len(front) < 3: + # Not enough points for knee detection + front_sorted = front.sort_values("wall_time") + return { + "fast": int(front_sorted.iloc[0]["idx"]), + "default": int(front_sorted.iloc[len(front_sorted) // 2]["idx"]), + "accurate": int(front_sorted.iloc[-1]["idx"]), + } + + # Sort by time (ascending) for kneedle + front = front.sort_values("wall_time").reset_index(drop=True) + x = front["wall_time"].values + y = front["f1"].values + + # Accurate: best F1 on the front + accurate_iloc = int(np.argmax(y)) + accurate_idx = int(front.iloc[accurate_iloc]["idx"]) + + # Fast: fastest Pareto-optimal point (lowest time, already sorted) + fast_iloc = 0 + fast_idx = int(front.iloc[0]["idx"]) + + # Default: use kneedle to find the knee point (diminishing returns) + default_idx = None + if KneeLocator is not None and len(x) >= 3: + try: + kn = KneeLocator( + x, y, + curve="concave", + direction="increasing", + S=1.0, + ) + if kn.knee is not None: + # Find the closest front point to the knee + knee_iloc = int(np.argmin(np.abs(x - kn.knee))) + default_idx = int(front.iloc[knee_iloc]["idx"]) + except Exception: + pass + + # Fallback: pick the point closest to 2/3 of the time range + if default_idx is None: + target_time = x[0] + 0.5 * (x[-1] - x[0]) + default_iloc = int(np.argmin(np.abs(x - target_time))) + default_idx = int(front.iloc[default_iloc]["idx"]) + + # Ensure fast < default < accurate in time and all different + # Find the iloc positions for ordering checks + default_iloc = int(front[front["idx"] == default_idx].index[0]) if default_idx is not None else None + if default_iloc is not None: + if default_iloc <= fast_iloc and fast_iloc + 1 < len(front): + default_iloc = fast_iloc + 1 + default_idx = int(front.iloc[default_iloc]["idx"]) + if default_iloc >= accurate_iloc and accurate_iloc > 0: + default_iloc = accurate_iloc - 1 + default_idx = int(front.iloc[default_iloc]["idx"]) + if default_idx == fast_idx: + if fast_iloc + 1 < len(front): + default_idx = int(front.iloc[fast_iloc + 1]["idx"]) + if default_idx == accurate_idx: + if accurate_iloc > 0: + default_idx = int(front.iloc[accurate_iloc - 1]["idx"]) + + return {"fast": fast_idx, "default": default_idx, "accurate": accurate_idx} + + +def _format_tier_config(row) -> str: + """Format a tier selection as copy-pasteable Python code.""" + lines = [ + f"# F1={row['f1']:.4f} TC={row['tc']:.4f} Time={row['wall_time']:.1f}s", + f"# Mode: {row['mode']}", + ] + # Per-run params + if row.get("run_details"): + for line in row["run_details"].split("\n"): + lines.append(f"# {line}") + lines.append(f"config = {{") + lines.append(f' "n_runs": {row["n_runs"]},') + lines.append(f' "vsm_amax": {row["vsm_amax"]},') + lines.append(f' "seq_weights": {row["seq_weights"]},') + lines.append(f' "consistency": {row["consistency"]},') + lines.append(f' "consistency_weight": {row["consistency_weight"]},') + lines.append(f' "realign": {row["realign"]},') + lines.append(f' "refine": "{row["refine"]}",') + lines.append(f' "min_support": {row["min_support"]},') + lines.append(f"}}") + return "\n".join(lines) + + +def create_app(ckpt_path: str, remote_path: str = "", refresh_sec: int = 30, + max_runs: int = 5): + app = Dash(__name__, title="Kalign Pareto Front") + + app.layout = html.Div([ + dcc.Store(id="ckpt-path", data=ckpt_path), + dcc.Store(id="remote-path", data=remote_path or ""), + dcc.Store(id="max-runs", data=max_runs), + dcc.Interval(id="refresh-interval", interval=refresh_sec * 1000, + disabled=(not remote_path)), + + html.H2("Kalign Unified Optimizer - Pareto Front"), + html.Div(id="status-bar", style={"marginBottom": "10px", "color": "#666"}), + + # Controls row + html.Div([ + html.Div([ + html.Label("Color by:"), + dcc.Dropdown( + id="color-by", + options=[ + {"label": "Mode", "value": "mode"}, + {"label": "VSM amax", "value": "vsm_amax"}, + {"label": "Seq weights", "value": "seq_weights"}, + {"label": "Consistency", "value": "consistency"}, + {"label": "Realign", "value": "realign"}, + {"label": "Refine", "value": "refine"}, + {"label": "Matrix (R0)", "value": "matrix_0"}, + {"label": "GPO (R0)", "value": "gpo_0"}, + ], + value="mode", + clearable=False, + ), + ], style={"width": "200px", "display": "inline-block", "marginRight": "20px"}), + html.Div([ + html.Label("X axis:"), + dcc.Dropdown( + id="x-axis", + options=[ + {"label": "Wall time (s)", "value": "wall_time"}, + {"label": "F1", "value": "f1"}, + {"label": "TC", "value": "tc"}, + ], + value="wall_time", + clearable=False, + ), + ], style={"width": "200px", "display": "inline-block", "marginRight": "20px"}), + html.Div([ + html.Label("Y axis:"), + dcc.Dropdown( + id="y-axis", + options=[ + {"label": "F1", "value": "f1"}, + {"label": "TC", "value": "tc"}, + {"label": "Wall time (s)", "value": "wall_time"}, + ], + value="f1", + clearable=False, + ), + ], style={"width": "200px", "display": "inline-block", "marginRight": "20px"}), + html.Div([ + html.Label("Filter mode:"), + dcc.Checklist( + id="mode-filter", + options=[ + {"label": "single", "value": "single"}, + {"label": "ens3", "value": "ens3"}, + {"label": "ens5", "value": "ens5"}, + ], + value=["single", "ens3", "ens5"], + inline=True, + ), + ], style={"display": "inline-block"}), + ], style={"marginBottom": "10px"}), + + # Main scatter plot + dcc.Graph(id="pareto-scatter", style={"height": "600px"}), + + # --- Tier Selection --- + html.H3("Default Configuration Selection"), + html.P("Auto-suggested tiers using knee-point detection on the F1-vs-time " + "Pareto front. Click any point on the 2D scatter plot, then assign " + "it to a tier with the buttons below.", + style={"color": "#666", "fontSize": "14px"}), + + html.Div([ + html.Div([ + html.Button("Set as Fast", id="set-fast-btn", n_clicks=0, + style={"backgroundColor": "#2196F3", "color": "white", + "border": "none", "padding": "8px 16px", + "marginRight": "10px", "cursor": "pointer"}), + html.Button("Set as Default", id="set-default-btn", n_clicks=0, + style={"backgroundColor": "#FF9800", "color": "white", + "border": "none", "padding": "8px 16px", + "marginRight": "10px", "cursor": "pointer"}), + html.Button("Set as Accurate", id="set-accurate-btn", n_clicks=0, + style={"backgroundColor": "#4CAF50", "color": "white", + "border": "none", "padding": "8px 16px", + "marginRight": "10px", "cursor": "pointer"}), + html.Button("Auto-select (kneedle)", id="auto-select-btn", n_clicks=0, + style={"backgroundColor": "#9E9E9E", "color": "white", + "border": "none", "padding": "8px 16px", + "cursor": "pointer"}), + ], style={"marginBottom": "10px"}), + ]), + + # Individual stores for each tier (avoids output=state on same component) + dcc.Store(id="tier-fast", data=None), + dcc.Store(id="tier-default", data=None), + dcc.Store(id="tier-accurate", data=None), + # Store for last clicked point idx + dcc.Store(id="last-clicked-idx", data=None), + + html.Div(id="tier-display", style={"marginBottom": "20px"}), + + html.Div([ + html.Button("Save tiers to JSON", id="save-tiers-btn", n_clicks=0, + style={"backgroundColor": "#673AB7", "color": "white", + "border": "none", "padding": "8px 16px", + "cursor": "pointer", "marginRight": "10px"}), + html.Span(id="save-status", style={"color": "#666", "fontSize": "14px"}), + ], style={"marginBottom": "20px"}), + + # 3D scatter + html.H3("3D Pareto Surface (F1 vs TC vs Time)"), + dcc.Graph(id="pareto-3d", style={"height": "600px"}), + + # Convergence plot + html.H3("Convergence (best F1 / TC over evaluations)"), + dcc.Graph(id="convergence-plot", style={"height": "350px"}), + + # Selected point details + html.H3("Click a point to see full parameters:"), + html.Pre(id="point-details", + style={"backgroundColor": "#f5f5f5", "padding": "15px", + "fontSize": "14px", "whiteSpace": "pre-wrap"}), + + # Top solutions table + html.H3("3D Pareto Front (non-dominated in F1, TC, and time)"), + dash_table.DataTable( + id="top-table", + columns=[ + {"name": "#", "id": "idx"}, + {"name": "Mode", "id": "mode"}, + {"name": "F1", "id": "f1"}, + {"name": "TC", "id": "tc"}, + {"name": "Time", "id": "wall_time"}, + {"name": "VSM", "id": "vsm_amax"}, + {"name": "SW", "id": "seq_weights"}, + {"name": "C", "id": "consistency"}, + {"name": "CW", "id": "consistency_weight"}, + {"name": "Re", "id": "realign"}, + {"name": "Ref", "id": "refine"}, + {"name": "MS", "id": "min_support"}, + {"name": "GPO(R0)", "id": "gpo_0"}, + {"name": "GPE(R0)", "id": "gpe_0"}, + {"name": "TGPE(R0)", "id": "tgpe_0"}, + {"name": "Mat(R0)", "id": "matrix_0"}, + ], + style_cell={"textAlign": "right", "padding": "4px", "fontSize": "13px"}, + style_header={"fontWeight": "bold"}, + style_data_conditional=[ + {"if": {"column_id": "mode"}, "textAlign": "left"}, + {"if": {"column_id": "refine"}, "textAlign": "center"}, + ], + sort_action="native", + row_selectable="single", + page_size=50, + ), + ], style={"maxWidth": "1400px", "margin": "auto", "padding": "20px"}) + + # Store for current data; params_by_idx maps idx -> full decoded params dict + app._df_cache = {"df": None, "hist_df": None, "mtime": 0, "params_by_idx": {}} # type: ignore[attr-defined] + + def _load_data(ckpt_path_arg, remote_path_arg, max_runs_arg): + """Load or refresh data from checkpoint.""" + local_path = ckpt_path_arg + + # Sync from remote if configured + if remote_path_arg: + sync_remote(remote_path_arg, local_path) + + if not Path(local_path).exists(): + return None, None, 0 + + mtime = Path(local_path).stat().st_mtime + if mtime == app._df_cache["mtime"]: # type: ignore[attr-defined] + return app._df_cache["df"], app._df_cache["hist_df"], mtime # type: ignore[attr-defined] + + try: + ckpt = load_checkpoint(local_path) + except Exception: + return app._df_cache["df"], app._df_cache["hist_df"], app._df_cache["mtime"] # type: ignore[attr-defined] + + mr = ckpt.get("max_runs", max_runs_arg) + df, params_by_idx = build_pareto_df(ckpt, mr) + hist_df = build_history_df(ckpt, mr) + n_gen = ckpt.get("n_gen_completed", "?") + + app._df_cache = {"df": df, "hist_df": hist_df, "mtime": mtime, "n_gen": n_gen, "params_by_idx": params_by_idx} # type: ignore[attr-defined] + return df, hist_df, mtime + + @app.callback( + Output("pareto-scatter", "figure"), + Output("pareto-3d", "figure"), + Output("convergence-plot", "figure"), + Output("top-table", "data"), + Output("status-bar", "children"), + Input("refresh-interval", "n_intervals"), + Input("color-by", "value"), + Input("x-axis", "value"), + Input("y-axis", "value"), + Input("mode-filter", "value"), + Input("tier-fast", "data"), + Input("tier-default", "data"), + Input("tier-accurate", "data"), + Input("ckpt-path", "data"), + Input("remote-path", "data"), + Input("max-runs", "data"), + ) + def update_all(n_intervals, color_by, x_axis, y_axis, mode_filter, + tier_fast, tier_default, tier_accurate, + ckpt_path, remote_path, max_runs): + tier_selections = {"fast": tier_fast, "default": tier_default, "accurate": tier_accurate} + df, hist_df, mtime = _load_data(ckpt_path, remote_path or None, max_runs) + + empty_fig = go.Figure() + if df is None or df.empty: + return empty_fig, empty_fig, empty_fig, [], "No data loaded" + + # Filter by mode + filtered = df[df["mode"].isin(mode_filter)] if mode_filter else df + + # Status + n_gen = app._df_cache.get("n_gen", "?") + ago = time.time() - mtime if mtime else 0 + status = (f"Generation {n_gen} | {len(df)} individuals | " + f"Best F1={df['f1'].max():.4f} | Best TC={df['tc'].max():.4f} | " + f"Last update: {ago:.0f}s ago") + + # 2D scatter + hover_data = ["mode", "vsm_amax", "seq_weights", "consistency", + "realign", "refine", "gpo_0", "gpe_0", "tgpe_0", "matrix_0", + "min_support"] + fig2d = px.scatter( + filtered, x=x_axis, y=y_axis, color=color_by, + hover_data=hover_data, + title=f"Population ({y_axis} vs {x_axis})", + template="plotly_white", + ) + fig2d.update_traces(marker=dict(size=8, opacity=0.7)) + + # Compute and draw overall 2D Pareto front line + pareto_line = _compute_pareto_2d(filtered, x_axis, y_axis) + if len(pareto_line) > 1: + fig2d.add_trace(go.Scatter( + x=pareto_line[x_axis].tolist(), + y=pareto_line[y_axis].tolist(), + mode="lines+markers", + name="Pareto front (all)", + line=dict(color="rgba(0,0,0,0.7)", width=2.5), + marker=dict(size=10, symbol="diamond", color="rgba(0,0,0,0.7)"), + hovertext=[ + f"F1={r['f1']:.4f} TC={r['tc']:.4f} t={r['wall_time']:.0f}s " + f"{r['mode']}" + for _, r in pareto_line.iterrows() + ], + hoverinfo="text", + )) + + # Per-mode Pareto front lines + mode_colors = {"single": "rgba(31,119,180,0.6)", + "ens3": "rgba(255,127,14,0.6)", + "ens5": "rgba(44,160,44,0.6)"} + for mode_name in sorted(filtered["mode"].unique()): + mode_df = filtered[filtered["mode"] == mode_name] + mode_pareto = _compute_pareto_2d(mode_df, x_axis, y_axis) + if len(mode_pareto) > 1: + fig2d.add_trace(go.Scatter( + x=mode_pareto[x_axis].tolist(), + y=mode_pareto[y_axis].tolist(), + mode="lines", + name=f"Pareto ({mode_name})", + line=dict(color=mode_colors.get(mode_name, "rgba(128,128,128,0.5)"), + width=1.5, dash="dash"), + hoverinfo="skip", + showlegend=True, + )) + + # Tier star markers + if tier_selections: + tier_styles = [ + ("fast", "Fast", "#2196F3"), + ("default", "Default", "#FF9800"), + ("accurate", "Accurate", "#4CAF50"), + ] + for key, label, color in tier_styles: + idx = tier_selections.get(key) + if idx is None: + continue + match = filtered[filtered["idx"] == idx] + if match.empty: + match = df[df["idx"] == idx] + if match.empty: + continue + r = match.iloc[0] + fig2d.add_trace(go.Scatter( + x=[r[x_axis]], y=[r[y_axis]], + mode="markers+text", + name=f"★ {label}", + marker=dict(size=18, symbol="star", color=color, + line=dict(width=2, color="black")), + text=[label], textposition="top center", + textfont=dict(size=12, color=color), + hovertext=f"{label}: F1={r['f1']:.4f} TC={r['tc']:.4f} t={r['wall_time']:.0f}s", + hoverinfo="text", + )) + + fig2d.update_layout(height=600) + + # 3D scatter + fig3d = px.scatter_3d( + filtered, x="wall_time", y="f1", z="tc", color=color_by, + hover_data=hover_data, + title="3D Pareto Surface", + template="plotly_white", + ) + fig3d.update_traces(marker=dict(size=5, opacity=0.7)) + fig3d.update_layout(height=600, scene=dict( + xaxis_title="Wall time (s)", + yaxis_title="F1", + zaxis_title="TC", + )) + + # Convergence + if hist_df is not None and not hist_df.empty: + hist_df = hist_df.copy() + hist_df["best_f1"] = hist_df["f1"].cummax() + hist_df["best_tc"] = hist_df["tc"].cummax() + fig_conv = go.Figure() + fig_conv.add_trace(go.Scatter( + x=hist_df["eval"], y=hist_df["best_f1"], + mode="lines", name="Best F1", + )) + fig_conv.add_trace(go.Scatter( + x=hist_df["eval"], y=hist_df["best_tc"], + mode="lines", name="Best TC", + )) + fig_conv.update_layout( + template="plotly_white", height=350, + xaxis_title="Evaluation #", yaxis_title="Score", + ) + else: + fig_conv = empty_fig + + # Pareto front table (3D: maximize F1, maximize TC, minimize time) + pareto_3d = _compute_pareto_3d(filtered) + pareto_3d = pareto_3d.sort_values("f1", ascending=False) + table_data = pareto_3d.drop(columns=["run_details"]).to_dict("records") + + return fig2d, fig3d, fig_conv, table_data, status + + @app.callback( + Output("last-clicked-idx", "data"), + Input("pareto-scatter", "clickData"), + State("mode-filter", "value"), + State("x-axis", "value"), + State("y-axis", "value"), + ) + def track_click(click_data, mode_filter, x_axis, y_axis): + """Track the idx of the last clicked point on the 2D scatter.""" + df = app._df_cache.get("df") + if df is None or click_data is None: + return None + pts = click_data.get("points", []) + if not pts: + return None + pt = pts[0] + x_val = pt.get("x") + y_val = pt.get("y") + if x_val is not None and y_val is not None: + filtered = df[df["mode"].isin(mode_filter)] if mode_filter else df + if filtered.empty: + return None + # Normalize distances by range to handle different scales + x_range = filtered[x_axis].max() - filtered[x_axis].min() + y_range = filtered[y_axis].max() - filtered[y_axis].min() + x_range = max(x_range, 1e-10) + y_range = max(y_range, 1e-10) + dists = ((filtered[x_axis] - x_val) / x_range)**2 + \ + ((filtered[y_axis] - y_val) / y_range)**2 + best = dists.idxmin() + return int(filtered.loc[best, "idx"]) + return None + + @app.callback( + Output("tier-fast", "data"), + Output("tier-default", "data"), + Output("tier-accurate", "data"), + Input("set-fast-btn", "n_clicks"), + Input("set-default-btn", "n_clicks"), + Input("set-accurate-btn", "n_clicks"), + Input("auto-select-btn", "n_clicks"), + State("tier-fast", "data"), + State("tier-default", "data"), + State("tier-accurate", "data"), + State("last-clicked-idx", "data"), + State("mode-filter", "value"), + ) + def update_tiers(_fc, _dc, _ac, _auto, + cur_fast, cur_default, cur_accurate, last_idx, mode_filter): + """Update tier selections based on button clicks.""" + triggered_id = ctx.triggered_id + if triggered_id is None: + return cur_fast, cur_default, cur_accurate + + if triggered_id == "auto-select-btn": + df = app._df_cache.get("df") + if df is not None and not df.empty: + filtered = df[df["mode"].isin(mode_filter)] if mode_filter else df + tiers = auto_select_tiers(filtered) + return tiers.get("fast"), tiers.get("default"), tiers.get("accurate") + return cur_fast, cur_default, cur_accurate + + if last_idx is None: + return cur_fast, cur_default, cur_accurate + + if triggered_id == "set-fast-btn": + return last_idx, cur_default, cur_accurate + elif triggered_id == "set-default-btn": + return cur_fast, last_idx, cur_accurate + elif triggered_id == "set-accurate-btn": + return cur_fast, cur_default, last_idx + + return cur_fast, cur_default, cur_accurate + + @app.callback( + Output("tier-display", "children"), + Input("tier-fast", "data"), + Input("tier-default", "data"), + Input("tier-accurate", "data"), + ) + def render_tiers(tier_fast, tier_default, tier_accurate): + """Render the selected tier configurations.""" + df = app._df_cache.get("df") + tiers = {"fast": tier_fast, "default": tier_default, "accurate": tier_accurate} + if df is None: + return html.Div("No tiers selected yet.", style={"color": "#999"}) + + tier_names = [("fast", "Fast", "#2196F3"), + ("default", "Default", "#FF9800"), + ("accurate", "Accurate", "#4CAF50")] + cards = [] + + for key, label, color in tier_names: + idx = tiers.get(key) + if idx is None: + cards.append(html.Div([ + html.H4(f"{label}", style={"color": color, "marginBottom": "5px"}), + html.Span("Not set — click a point then press the button", + style={"color": "#999", "fontSize": "13px"}), + ], style={"display": "inline-block", "verticalAlign": "top", + "width": "32%", "marginRight": "1%", + "padding": "10px", "backgroundColor": "#fafafa", + "border": f"2px solid {color}", "borderRadius": "8px"})) + continue + + match = df[df["idx"] == idx] + if match.empty: + continue + row = match.iloc[0] + config_text = _format_tier_config(row) + cards.append(html.Div([ + html.H4(f"{label}", style={"color": color, "marginBottom": "5px"}), + html.Div(f"F1={row['f1']:.4f} TC={row['tc']:.4f} Time={row['wall_time']:.1f}s", + style={"fontWeight": "bold", "marginBottom": "5px"}), + html.Div(f"{row['mode']}", style={"fontSize": "13px", "marginBottom": "8px"}), + html.Pre(config_text, + style={"backgroundColor": "#f0f0f0", "padding": "8px", + "fontSize": "12px", "whiteSpace": "pre-wrap", + "margin": "0", "borderRadius": "4px"}), + ], style={"display": "inline-block", "verticalAlign": "top", + "width": "32%", "marginRight": "1%", + "padding": "10px", "backgroundColor": "#fafafa", + "border": f"2px solid {color}", "borderRadius": "8px"})) + + return html.Div(cards, style={"marginBottom": "20px"}) + + @app.callback( + Output("save-status", "children"), + Input("save-tiers-btn", "n_clicks"), + State("tier-fast", "data"), + State("tier-default", "data"), + State("tier-accurate", "data"), + State("ckpt-path", "data"), + ) + def save_tiers(n_clicks, tier_fast, tier_default, tier_accurate, ckpt_path): + """Save selected tiers to a JSON file next to the checkpoint.""" + tiers = {"fast": tier_fast, "default": tier_default, "accurate": tier_accurate} + if not n_clicks: + return "" + + df = app._df_cache.get("df") + params_by_idx = app._df_cache.get("params_by_idx", {}) + if df is None: + return "No data loaded." + + any_set = any(tiers.get(k) is not None for k in ("fast", "default", "accurate")) + if not any_set: + return "No tiers selected yet." + + output = {} + for tier_name in ("fast", "default", "accurate"): + idx = tiers.get(tier_name) + if idx is None: + continue + match = df[df["idx"] == idx] + if match.empty: + continue + row = match.iloc[0] + + # Full decoded params from the optimizer + full_params = params_by_idx.get(idx, {}) + + # Build per-run config list + runs = [] + for k in range(int(row["n_runs"])): + run = { + "gpo": round(float(full_params["run_gpo"][k]), 4), + "gpe": round(float(full_params["run_gpe"][k]), 4), + "tgpe": round(float(full_params["run_tgpe"][k]), 4), + "matrix": MATRIX_NAMES.get(full_params["run_types"][k], "?"), + } + if full_params["run_noise"][k] > 0: + run["noise"] = round(float(full_params["run_noise"][k]), 4) + runs.append(run) + + output[tier_name] = { + "scores": { + "f1": float(row["f1"]), + "tc": float(row["tc"]), + "wall_time": float(row["wall_time"]), + }, + "params": { + "n_runs": int(row["n_runs"]), + "vsm_amax": float(row["vsm_amax"]), + "seq_weights": float(row["seq_weights"]), + "consistency": int(row["consistency"]), + "consistency_weight": float(row["consistency_weight"]), + "realign": int(row["realign"]), + "refine": str(row["refine"]), + "min_support": int(row["min_support"]), + }, + "runs": runs, + } + + # Save next to checkpoint file + out_path = Path(ckpt_path).parent / "kalign_tiers.json" + with open(out_path, "w") as f: + json.dump(output, f, indent=2) + + n_tiers = len(output) + return f"Saved {n_tiers} tier(s) to {out_path}" + + @app.callback( + Output("point-details", "children"), + Input("pareto-scatter", "clickData"), + Input("pareto-3d", "clickData"), + Input("top-table", "selected_rows"), + State("top-table", "data"), + State("x-axis", "value"), + State("y-axis", "value"), + ) + def show_details(click_2d, click_3d, selected_rows, table_data, + x_axis, y_axis): + df = app._df_cache.get("df") + if df is None or df.empty: + return "Click a point or select a table row to see details." + + triggered_id = ctx.triggered_id + + # From table selection + if triggered_id == "top-table" and selected_rows and table_data: + row = table_data[selected_rows[0]] + idx = row["idx"] + match = df[df["idx"] == idx] + if not match.empty: + return _format_detail(match.iloc[0]) + + # From scatter click — match by coordinates + click = None + if triggered_id == "pareto-3d" and click_3d: + click = click_3d + elif triggered_id == "pareto-scatter" and click_2d: + click = click_2d + + if click and click.get("points"): + pt = click["points"][0] + x_val = pt.get("x") + y_val = pt.get("y") + if x_val is not None and y_val is not None: + # For 3D clicks, match on wall_time and f1 + if triggered_id == "pareto-3d": + x_col, y_col = "wall_time", "f1" + else: + x_col, y_col = x_axis, y_axis + x_range = max(df[x_col].max() - df[x_col].min(), 1e-10) + y_range = max(df[y_col].max() - df[y_col].min(), 1e-10) + dists = ((df[x_col] - x_val) / x_range)**2 + \ + ((df[y_col] - y_val) / y_range)**2 + best = dists.idxmin() + return _format_detail(df.loc[best]) + + return "Click a point or select a table row to see details." + + return app + + +def _format_detail(row): + """Format full details for a selected solution.""" + lines = [ + f"Solution #{row['idx']}", + f"{'='*50}", + f"F1 = {row['f1']:.4f} TC = {row['tc']:.4f} Time = {row['wall_time']:.1f}s", + f"Mode: {row['mode']} n_runs={row['n_runs']}", + f"", + f"Per-run parameters:", + row["run_details"], + f"", + f"Shared parameters:", + f" vsm_amax = {row['vsm_amax']}", + f" seq_weights = {row['seq_weights']}", + f" consistency = {row['consistency']}", + f" consistency_wt = {row['consistency_weight']}", + f" realign = {row['realign']}", + f" refine = {row['refine']}", + f" min_support = {row['min_support']}", + ] + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Interactive Pareto front viewer for kalign optimizer") + parser.add_argument("checkpoint", help="Path to gen_checkpoint.pkl (local)") + parser.add_argument("--remote", default=None, + help="Remote path (e.g. server:path/gen_checkpoint.pkl) to auto-sync") + parser.add_argument("--port", type=int, default=8050, + help="Dash server port (default: 8050)") + parser.add_argument("--refresh", type=int, default=30, + help="Auto-refresh interval in seconds (default: 30, remote only)") + parser.add_argument("--max-runs", type=int, default=5, + help="Max ensemble runs (must match optimizer, default: 5)") + args = parser.parse_args() + + # If remote specified, do initial sync + if args.remote: + print(f"Syncing from {args.remote}...") + sync_remote(args.remote, args.checkpoint) + + if not Path(args.checkpoint).exists(): + print(f"Checkpoint not found: {args.checkpoint}") + return + + app = create_app( + ckpt_path=args.checkpoint, + remote_path=args.remote or "", + refresh_sec=args.refresh, + max_runs=args.max_runs, + ) + + print(f"Starting Pareto viewer on http://localhost:{args.port}") + if args.remote: + print(f"Auto-refreshing from {args.remote} every {args.refresh}s") + app.run(debug=False, port=args.port) + + +if __name__ == "__main__": + main() diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 553bda5..09e115e 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -2,6 +2,7 @@ #define KALIGN_H #include +#include #ifdef KALIGN_IMPORT #define EXTERN @@ -85,6 +86,18 @@ EXTERN int kalign_ensemble(struct msa* msa, int n_threads, int type, int realign, float use_seq_weights, int consistency_anchors, float consistency_weight); +EXTERN int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, + int n_runs, + const float* run_gpo, + const float* run_gpe, + const float* run_tgpe, + const int* run_types, + const float* run_noise, + uint64_t seed, int min_support, + int refine, float vsm_amax, + int realign, float use_seq_weights, + int consistency_anchors, float consistency_weight); + EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); @@ -107,6 +120,24 @@ EXTERN int kalign_msa_compare_detailed(struct msa *r, struct msa *t, EXTERN int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, int *scored_cols, int n_cols, struct poar_score *out); + +/* Unified alignment entry point — all callers should use this. */ +EXTERN struct kalign_run_config kalign_run_config_defaults(void); +EXTERN struct kalign_ensemble_config kalign_ensemble_config_defaults(void); + +EXTERN int kalign_align_full(struct msa* msa, + const struct kalign_run_config* runs, + int n_runs, + const struct kalign_ensemble_config* ens, + int n_threads); + +/* Expand a base config into n_runs configs using the built-in diversity table. + Caller must pass resolved (non-sentinel) gap penalties in base. + Caller allocates out[n_runs]. */ +EXTERN int kalign_generate_ensemble_runs(const struct kalign_run_config* base, + int n_runs, uint64_t seed, + struct kalign_run_config* out); + #undef KALIGN_IMPORT #undef EXTERN diff --git a/lib/include/kalign/kalign_config.h b/lib/include/kalign/kalign_config.h new file mode 100644 index 0000000..02b15f2 --- /dev/null +++ b/lib/include/kalign/kalign_config.h @@ -0,0 +1,36 @@ +#ifndef KALIGN_CONFIG_H +#define KALIGN_CONFIG_H + +#include +#include + +/* Per-run alignment configuration. + Each field controls one aspect of a single alignment run. + Use kalign_run_config_defaults() to get a config with all sentinels/defaults, + then override only the fields you care about. */ +struct kalign_run_config { + int type; /* KALIGN_TYPE_* constant (UNDEFINED = auto-detect) */ + float gpo; /* gap open penalty (-1.0 = use matrix default) */ + float gpe; /* gap extend penalty (-1.0 = use matrix default) */ + float tgpe; /* terminal gap extend penalty (-1.0 = use matrix default) */ + float vsm_amax; /* variable scoring matrix amplitude (-1.0 = biotype default) */ + float dist_scale; /* distance-dependent gap scaling (0.0 = off) */ + float use_seq_weights; /* profile rebalancing pseudocount (-1.0 = biotype default) */ + int consistency_anchors; /* consistency transform: K anchor sequences (0 = off) */ + float consistency_weight; /* consistency transform: bonus scale (default: 2.0) */ + int refine; /* KALIGN_REFINE_* constant (default: NONE) */ + int adaptive_budget; /* scale refinement trials by uncertainty (0 = off) */ + int realign; /* iterative tree-rebuild iterations (0 = off) */ + uint64_t tree_seed; /* random seed for guide tree perturbation (0 = deterministic) */ + float tree_noise; /* guide tree perturbation sigma (0.0 = none) */ +}; + +/* Ensemble orchestration configuration. + Only used when n_runs > 1. */ +struct kalign_ensemble_config { + uint64_t seed; /* base RNG seed for diversity generation */ + int min_support; /* POAR consensus threshold (0 = auto) */ + const char* save_poar; /* path to save POAR table (NULL = don't save) */ +}; + +#endif diff --git a/lib/src/aln_mem.c b/lib/src/aln_mem.c index 49a0d0b..415ccf4 100644 --- a/lib/src/aln_mem.c +++ b/lib/src/aln_mem.c @@ -38,7 +38,6 @@ int alloc_aln_mem(struct aln_mem** mem, int x) m->ap = NULL; m->consistency = NULL; - m->consistency_stride = 0; m->starta = 0; m->startb = 0; diff --git a/lib/src/aln_profileprofile.c b/lib/src/aln_profileprofile.c index 2b0acc0..d41d948 100644 --- a/lib/src/aln_profileprofile.c +++ b/lib/src/aln_profileprofile.c @@ -6,6 +6,7 @@ #include "aln_param.h" #include "aln_struct.h" +#include "anchor_consistency.h" #define ALN_PROFILEPROFILE_IMPORT #include "aln_profileprofile.h" @@ -106,7 +107,7 @@ int aln_profileprofile_foward(struct aln_mem* m) } prof2 -= 32; if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, i, j); } s[j].a = pa; @@ -136,7 +137,7 @@ int aln_profileprofile_foward(struct aln_mem* m) } prof2 -= 32; if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, i, j); } s[j].a = pa; @@ -249,7 +250,7 @@ int aln_profileprofile_backward(struct aln_mem* m) } prof2 -= 32; if(m->consistency){ - pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, m->starta_2 + i, j); } s[j].a = pa; @@ -278,7 +279,7 @@ int aln_profileprofile_backward(struct aln_mem* m) } prof2 -= 32; if(m->consistency){ - pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, m->starta_2 + i, j); } s[j].a = pa; diff --git a/lib/src/aln_refine.c b/lib/src/aln_refine.c index 794e64d..64cbe02 100644 --- a/lib/src/aln_refine.c +++ b/lib/src/aln_refine.c @@ -149,7 +149,6 @@ int refine_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int /* Compute consistency bonus for all merge types */ ml->consistency = NULL; - ml->consistency_stride = 0; { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ @@ -180,7 +179,6 @@ int refine_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &ml->consistency)); - ml->consistency_stride = dp_cols; } } @@ -288,9 +286,8 @@ int refine_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int /* Free consistency bonus */ if(ml->consistency){ - MFREE(ml->consistency); + sparse_bonus_free(ml->consistency); ml->consistency = NULL; - ml->consistency_stride = 0; } /* Update confidence from best trial */ @@ -403,7 +400,6 @@ int replay_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int /* Compute consistency bonus for all merge types */ ml->consistency = NULL; - ml->consistency_stride = 0; { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ @@ -434,7 +430,6 @@ int replay_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &ml->consistency)); - ml->consistency_stride = dp_cols; } } @@ -442,9 +437,8 @@ int replay_edge(struct msa* msa, struct aln_param* ap, struct aln_tasks* t, int /* Free consistency bonus */ if(ml->consistency){ - MFREE(ml->consistency); + sparse_bonus_free(ml->consistency); ml->consistency = NULL; - ml->consistency_stride = 0; } /* Store alignment confidence */ diff --git a/lib/src/aln_run.c b/lib/src/aln_run.c index 335ae1a..45f6e6d 100644 --- a/lib/src/aln_run.c +++ b/lib/src/aln_run.c @@ -257,7 +257,6 @@ int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id) m->margin_sum = 0.0F; m->margin_count = 0; m->consistency = NULL; - m->consistency_stride = 0; /* Compute consistency bonus for all merge types */ { @@ -290,7 +289,6 @@ int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id) RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &m->consistency)); - m->consistency_stride = dp_cols; } } @@ -399,9 +397,8 @@ int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id) /* Free consistency bonus if allocated */ if(m->consistency){ - MFREE(m->consistency); + sparse_bonus_free(m->consistency); m->consistency = NULL; - m->consistency_stride = 0; } /* Restore original aln_param for profile update (unscaled base penalties) */ @@ -564,7 +561,6 @@ int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, /* Compute consistency bonus for all merge types */ m->consistency = NULL; - m->consistency_stride = 0; { struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; if(ct != NULL){ @@ -595,7 +591,6 @@ int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, RUN(anchor_consistency_get_bonus_profile(ct, msa, dp_row_node, dp_rows, dp_col_node, dp_cols, &m->consistency)); - m->consistency_stride = dp_cols; } } @@ -734,9 +729,8 @@ int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, /* Free consistency bonus if allocated */ if(m->consistency){ - MFREE(m->consistency); + sparse_bonus_free(m->consistency); m->consistency = NULL; - m->consistency_stride = 0; } /* Store confidence */ diff --git a/lib/src/aln_seqprofile.c b/lib/src/aln_seqprofile.c index 1a58962..3b8cfe0 100644 --- a/lib/src/aln_seqprofile.c +++ b/lib/src/aln_seqprofile.c @@ -5,6 +5,7 @@ #include "aln_param.h" #include "aln_struct.h" +#include "anchor_consistency.h" #define ALN_SEQPROFILE_IMPORT #include "aln_seqprofile.h" #define MAX(a, b) (a > b ? a : b) @@ -80,7 +81,7 @@ int aln_seqprofile_foward(struct aln_mem* m) pa += prof1[32 + seq2[j]]; if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, i, j); } s[j].a = pa; @@ -105,7 +106,7 @@ int aln_seqprofile_foward(struct aln_mem* m) pa += prof1[32 + seq2[j]]; if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, i, j); } s[j].a = pa; @@ -191,7 +192,7 @@ int aln_seqprofile_backward(struct aln_mem* m) pa = MAX3(pa,pga - open,pgb +prof1[91]); pa += prof1[32 + seq2[j]]; if(m->consistency){ - pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, m->starta_2 + i, j); } s[j].a = pa; @@ -214,7 +215,7 @@ int aln_seqprofile_backward(struct aln_mem* m) pa = MAX3(pa,pga - open,pgb +prof1[91]); pa += prof1[32 + seq2[j]]; if(m->consistency){ - pa += m->consistency[(m->starta_2 + i) * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, m->starta_2 + i, j); } s[j].a = pa; diff --git a/lib/src/aln_seqseq.c b/lib/src/aln_seqseq.c index d74b70b..eabb516 100644 --- a/lib/src/aln_seqseq.c +++ b/lib/src/aln_seqseq.c @@ -6,6 +6,7 @@ #include "aln_param.h" #include "aln_struct.h" +#include "anchor_consistency.h" #define ALN_SEQSEQ_IMPORT #include "aln_seqseq.h" @@ -81,7 +82,7 @@ int aln_seqseq_foward(struct aln_mem* m) pa = MAX3(pa,pga-gpo,pgb-gpo); pa += subp[seq2[j]] - soff; if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, i, j); } s[j].a = pa; @@ -103,7 +104,7 @@ int aln_seqseq_foward(struct aln_mem* m) pa = MAX3(pa,pga-gpo,pgb-gpo); pa += subp[seq2[j]] - soff; if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, i, j); } s[j].a = pa; @@ -197,7 +198,7 @@ int aln_seqseq_backward(struct aln_mem* m) pa += subp[seq2[j]] - soff; if(m->consistency){ - pa += m->consistency[(starta + i) * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, starta + i, j); } s[j].a = pa; @@ -221,7 +222,7 @@ int aln_seqseq_backward(struct aln_mem* m) pa += subp[seq2[j]] - soff; if(m->consistency){ - pa += m->consistency[(starta + i) * m->consistency_stride + j]; + pa += sparse_bonus_lookup(m->consistency, starta + i, j); } s[j].a = pa; diff --git a/lib/src/aln_struct.h b/lib/src/aln_struct.h index 26c7128..5cf9dcc 100644 --- a/lib/src/aln_struct.h +++ b/lib/src/aln_struct.h @@ -6,6 +6,8 @@ #define ALN_MODE_SCORE_ONLY 2 #define ALN_MODE_FULL 1 +struct sparse_bonus; + struct states{ float a; float ga; @@ -54,8 +56,7 @@ struct aln_mem{ int sip; int mode; - float* consistency; /* bonus matrix [i * consistency_stride + j], NULL if disabled */ - int consistency_stride; /* = len_b (stride for j dimension) */ + struct sparse_bonus* consistency; /* sparse bonus matrix, NULL if disabled */ }; #endif diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index a051fb5..6e2468c 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -18,6 +18,8 @@ #include "aln_apair_dist.h" #include "anchor_consistency.h" #include "kalign/kalign.h" +#include "kalign/kalign_config.h" +#include "ensemble.h" #ifdef HAVE_OPENMP #include @@ -672,3 +674,76 @@ int kalign_post_realign(struct msa *msa, int n_threads, int type, return FAIL; } +/* ======================================================================== */ +/* Config defaults and unified entry point */ +/* ======================================================================== */ + +struct kalign_run_config kalign_run_config_defaults(void) +{ + struct kalign_run_config cfg; + cfg.type = KALIGN_TYPE_UNDEFINED; + cfg.gpo = -1.0f; + cfg.gpe = -1.0f; + cfg.tgpe = -1.0f; + cfg.vsm_amax = -1.0f; + cfg.dist_scale = 0.0f; + cfg.use_seq_weights = -1.0f; + cfg.consistency_anchors = 0; + cfg.consistency_weight = 2.0f; + cfg.refine = KALIGN_REFINE_NONE; + cfg.adaptive_budget = 0; + cfg.realign = 0; + cfg.tree_seed = 0; + cfg.tree_noise = 0.0f; + return cfg; +} + +struct kalign_ensemble_config kalign_ensemble_config_defaults(void) +{ + struct kalign_ensemble_config ens; + ens.seed = 42; + ens.min_support = 0; + ens.save_poar = NULL; + return ens; +} + +int kalign_align_full(struct msa* msa, + const struct kalign_run_config* runs, + int n_runs, + const struct kalign_ensemble_config* ens, + int n_threads) +{ + ASSERT(msa != NULL, "No MSA"); + ASSERT(runs != NULL, "No run configs"); + ASSERT(n_runs >= 1, "n_runs must be >= 1"); + + if(n_runs > 1){ + /* Ensemble path */ + RUN(kalign_ensemble_from_configs(msa, runs, n_runs, ens, n_threads)); + }else{ + /* Single-run path */ + const struct kalign_run_config* r = &runs[0]; + if(r->realign > 0){ + RUN(kalign_run_realign(msa, n_threads, r->type, + r->gpo, r->gpe, r->tgpe, + r->refine, r->adaptive_budget, + r->dist_scale, r->vsm_amax, + r->realign, r->use_seq_weights, + r->consistency_anchors, + r->consistency_weight)); + }else{ + RUN(kalign_run_seeded(msa, n_threads, r->type, + r->gpo, r->gpe, r->tgpe, + r->refine, r->adaptive_budget, + r->tree_seed, r->tree_noise, + r->dist_scale, r->vsm_amax, + r->use_seq_weights, + r->consistency_anchors, + r->consistency_weight)); + } + } + + return OK; +ERROR: + return FAIL; +} diff --git a/lib/src/aln_wrap.h b/lib/src/aln_wrap.h index dade933..97452d5 100644 --- a/lib/src/aln_wrap.h +++ b/lib/src/aln_wrap.h @@ -2,6 +2,7 @@ #define ALN_WRAP_H #include +#include #ifdef ALN_WRAP_IMPORT #define EXTERN @@ -33,6 +34,12 @@ EXTERN int kalign_run_realign(struct msa *msa, int n_threads, int type, float use_seq_weights, int consistency_anchors, float consistency_weight); +EXTERN int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, + float gpo, float gpe, float tgpe, + int refine, int adaptive_budget, + float dist_scale, float vsm_amax, + float use_seq_weights); + EXTERN int kalign_post_realign(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget, @@ -40,6 +47,15 @@ EXTERN int kalign_post_realign(struct msa *msa, int n_threads, int type, int realign_iterations, float use_seq_weights); +EXTERN struct kalign_run_config kalign_run_config_defaults(void); +EXTERN struct kalign_ensemble_config kalign_ensemble_config_defaults(void); + +EXTERN int kalign_align_full(struct msa* msa, + const struct kalign_run_config* runs, + int n_runs, + const struct kalign_ensemble_config* ens, + int n_threads); + #undef ALN_WRAP_IMPORT #undef EXTERN diff --git a/lib/src/anchor_consistency.c b/lib/src/anchor_consistency.c index c567d9d..43fa678 100644 --- a/lib/src/anchor_consistency.c +++ b/lib/src/anchor_consistency.c @@ -274,19 +274,38 @@ int anchor_consistency_build(struct msa* msa, struct aln_param* ap, return FAIL; } +void sparse_bonus_free(struct sparse_bonus* sb) +{ + if(sb){ + if(sb->cols) MFREE(sb->cols); + if(sb->vals) MFREE(sb->vals); + MFREE(sb); + } +} + int anchor_consistency_get_bonus(struct consistency_table* ct, int seq_a, int len_a, int seq_b, int len_b, - float** bonus_out) + struct sparse_bonus** bonus_out) { - float* bonus = NULL; + struct sparse_bonus* sb = NULL; int* inv_b = NULL; int K = ct->n_anchors; int k, i; float per_anchor_weight = ct->weight / (float)K; - MMALLOC(bonus, sizeof(float) * len_a * len_b); - memset(bonus, 0, sizeof(float) * len_a * len_b); + MMALLOC(sb, sizeof(struct sparse_bonus)); + sb->cols = NULL; + sb->vals = NULL; + sb->n_rows = len_a; + sb->K = K; + + MMALLOC(sb->cols, sizeof(int) * len_a * K); + MMALLOC(sb->vals, sizeof(float) * len_a * K); + for(i = 0; i < len_a * K; i++){ + sb->cols[i] = -1; + sb->vals[i] = 0.0f; + } /* For each anchor, build inverse map for seq_b (anchor_pos → seq_b_pos), then scan seq_a's map to accumulate bonus. O(K * (La + Lb + max_anchor_len)) */ @@ -298,8 +317,6 @@ int anchor_consistency_get_bonus(struct consistency_table* ct, if(map_a == NULL || map_b == NULL) continue; - /* Determine anchor length for inverse map */ - /* Find max mapped position to size the inverse map */ anchor_len = 0; for(i = 0; i < len_a; i++){ if(map_a[i] >= anchor_len) anchor_len = map_a[i] + 1; @@ -309,7 +326,6 @@ int anchor_consistency_get_bonus(struct consistency_table* ct, } if(anchor_len == 0) continue; - /* Build inverse map: anchor_pos → seq_b_pos */ MMALLOC(inv_b, sizeof(int) * anchor_len); for(j = 0; j < anchor_len; j++){ inv_b[j] = -1; @@ -320,13 +336,23 @@ int anchor_consistency_get_bonus(struct consistency_table* ct, } } - /* Accumulate bonus */ + /* Accumulate bonus into sparse slots */ for(i = 0; i < len_a; i++){ int ak_pos = map_a[i]; if(ak_pos >= 0 && ak_pos < anchor_len){ int bj = inv_b[ak_pos]; if(bj >= 0){ - bonus[i * len_b + bj] += per_anchor_weight; + int base = i * K; + int slot = -1; + int s; + for(s = 0; s < K; s++){ + if(sb->cols[base + s] == bj){ slot = s; break; } + if(sb->cols[base + s] < 0){ slot = s; break; } + } + if(slot >= 0){ + sb->vals[base + slot] += per_anchor_weight; + sb->cols[base + slot] = bj; + } } } } @@ -335,10 +361,10 @@ int anchor_consistency_get_bonus(struct consistency_table* ct, inv_b = NULL; } - *bonus_out = bonus; + *bonus_out = sb; return OK; ERROR: - if(bonus) MFREE(bonus); + sparse_bonus_free(sb); if(inv_b) MFREE(inv_b); *bonus_out = NULL; return FAIL; @@ -470,9 +496,9 @@ int anchor_consistency_get_bonus_profile(struct consistency_table* ct, struct msa* msa, int node_a, int len_a, int node_b, int len_b, - float** bonus_out) + struct sparse_bonus** bonus_out) { - float* bonus = NULL; + struct sparse_bonus* sb = NULL; int* apos_a = NULL; float* conf_a = NULL; int* apos_b = NULL; @@ -483,8 +509,18 @@ int anchor_consistency_get_bonus_profile(struct consistency_table* ct, int k, i, j; float per_anchor_weight = ct->weight / (float)K; - MMALLOC(bonus, sizeof(float) * len_a * len_b); - memset(bonus, 0, sizeof(float) * len_a * len_b); + MMALLOC(sb, sizeof(struct sparse_bonus)); + sb->cols = NULL; + sb->vals = NULL; + sb->n_rows = len_a; + sb->K = K; + + MMALLOC(sb->cols, sizeof(int) * len_a * K); + MMALLOC(sb->vals, sizeof(float) * len_a * K); + for(i = 0; i < len_a * K; i++){ + sb->cols[i] = -1; + sb->vals[i] = 0.0f; + } MMALLOC(apos_a, sizeof(int) * len_a); MMALLOC(conf_a, sizeof(float) * len_a); @@ -523,14 +559,24 @@ int anchor_consistency_get_bonus_profile(struct consistency_table* ct, } } - /* Accumulate bonus */ + /* Accumulate bonus into sparse slots */ for(i = 0; i < len_a; i++){ int ak_pos = apos_a[i]; if(ak_pos >= 0 && ak_pos < anchor_len){ int bj = inv_b[ak_pos]; if(bj >= 0){ - bonus[i * len_b + bj] += - per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; + float val = per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; + int base = i * K; + int slot = -1; + int s; + for(s = 0; s < K; s++){ + if(sb->cols[base + s] == bj){ slot = s; break; } + if(sb->cols[base + s] < 0){ slot = s; break; } + } + if(slot >= 0){ + sb->vals[base + slot] += val; + sb->cols[base + slot] = bj; + } } } } @@ -546,10 +592,10 @@ int anchor_consistency_get_bonus_profile(struct consistency_table* ct, MFREE(apos_b); MFREE(conf_b); - *bonus_out = bonus; + *bonus_out = sb; return OK; ERROR: - if(bonus) MFREE(bonus); + sparse_bonus_free(sb); if(apos_a) MFREE(apos_a); if(conf_a) MFREE(conf_a); if(apos_b) MFREE(apos_b); diff --git a/lib/src/anchor_consistency.h b/lib/src/anchor_consistency.h index 4f46246..5fc3884 100644 --- a/lib/src/anchor_consistency.h +++ b/lib/src/anchor_consistency.h @@ -23,6 +23,28 @@ struct consistency_table { float weight; /* scaling factor for bonus */ }; +struct sparse_bonus { + int* cols; /* cols[i * K + k] = column index, or -1 if unused */ + float* vals; /* vals[i * K + k] = bonus value */ + int n_rows; /* = len_a (number of DP rows) */ + int K; /* max entries per row (= n_anchors) */ +}; + +static inline float sparse_bonus_lookup(const struct sparse_bonus* sb, int i, int j) +{ + float bonus = 0.0f; + const int base = i * sb->K; + int k; + for(k = 0; k < sb->K; k++){ + if(sb->cols[base + k] < 0) break; + if(sb->cols[base + k] == j) + bonus += sb->vals[base + k]; + } + return bonus; +} + +EXTERN void sparse_bonus_free(struct sparse_bonus* sb); + EXTERN int anchor_consistency_build(struct msa* msa, struct aln_param* ap, int n_anchors, float weight, struct consistency_table** ct_out); @@ -30,13 +52,13 @@ EXTERN int anchor_consistency_build(struct msa* msa, struct aln_param* ap, EXTERN int anchor_consistency_get_bonus(struct consistency_table* ct, int seq_a, int len_a, int seq_b, int len_b, - float** bonus_out); + struct sparse_bonus** bonus_out); EXTERN int anchor_consistency_get_bonus_profile(struct consistency_table* ct, struct msa* msa, int node_a, int len_a, int node_b, int len_b, - float** bonus_out); + struct sparse_bonus** bonus_out); EXTERN void anchor_consistency_free(struct consistency_table* ct); diff --git a/lib/src/ensemble.c b/lib/src/ensemble.c index bfc2bc0..6c806c5 100644 --- a/lib/src/ensemble.c +++ b/lib/src/ensemble.c @@ -13,6 +13,7 @@ #include "consensus_msa.h" #include "kalign/kalign.h" +#include "kalign/kalign_config.h" #define ENSEMBLE_IMPORT #include "ensemble.h" @@ -497,6 +498,539 @@ int kalign_ensemble(struct msa* msa, int n_threads, int type, return FAIL; } +/* ======================================================================== */ +/* kalign_ensemble_custom: like kalign_ensemble but with per-run parameters. + * + * Instead of a hardcoded scale-factor table, each run gets its own + * gap penalties, matrix type, and tree noise via arrays. + * + * run_gpo[n_runs], run_gpe[n_runs], run_tgpe[n_runs]: per-run gap penalties + * run_types[n_runs]: per-run matrix type (KALIGN_TYPE_PROTEIN, _PFASUM43, etc.) + * Pass NULL to use the same 'type' for all runs. + * run_noise[n_runs]: per-run tree noise sigma + * + * All other parameters (vsm_amax, realign, consistency, etc.) are shared + * across runs — they affect *how* each alignment is computed, not *what* + * gap/matrix parameters it uses. + */ +int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, + int n_runs, + const float* run_gpo, + const float* run_gpe, + const float* run_tgpe, + const int* run_types, + const float* run_noise, + uint64_t seed, int min_support, + int refine, float vsm_amax, + int realign, float use_seq_weights, + int consistency_anchors, float consistency_weight) +{ + struct msa* copy = NULL; + struct msa* consensus_msa = NULL; + struct msa** alignments = NULL; + struct poar_table* poar = NULL; + struct pos_matrix* pm = NULL; + double* scores = NULL; + int numseq; + int k; + int best_k = 0; + int use_consensus = 0; + + ASSERT(msa != NULL, "No MSA"); + ASSERT(n_runs >= 1, "n_runs must be >= 1"); + ASSERT(run_gpo != NULL, "run_gpo is NULL"); + ASSERT(run_gpe != NULL, "run_gpe is NULL"); + ASSERT(run_tgpe != NULL, "run_tgpe is NULL"); + ASSERT(run_noise != NULL, "run_noise is NULL"); + + if(use_seq_weights < 0.0f){ + use_seq_weights = 0.0f; + } + + RUN(kalign_essential_input_check(msa, 0)); + + numseq = msa->numseq; + + DECLARE_TIMER(t_ensemble); + if(!msa->quiet){ + LOG_MSG("Custom ensemble alignment with %d runs", n_runs); + } + START_TIMER(t_ensemble); + + if(msa->biotype == ALN_BIOTYPE_UNDEF){ + RUN(detect_alphabet(msa)); + } + + RUN(poar_table_alloc(&poar, numseq)); + MMALLOC(alignments, sizeof(struct msa*) * n_runs); + for(k = 0; k < n_runs; k++){ + alignments[k] = NULL; + } + + for(k = 0; k < n_runs; k++){ + int run_type = (run_types != NULL) ? run_types[k] : type; + uint64_t run_seed = seed + (uint64_t)k; + + copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = 1; + + if(!msa->quiet){ + LOG_MSG(" Run %d/%d (gpo=%.2f gpe=%.2f tgpe=%.2f noise=%.2f type=%d)", + k + 1, n_runs, run_gpo[k], run_gpe[k], run_tgpe[k], + run_noise[k], run_type); + } + + if(realign > 0){ + RUN(kalign_run_realign(copy, n_threads, run_type, + run_gpo[k], run_gpe[k], run_tgpe[k], + refine, 0, + 0.0f, vsm_amax, + realign, use_seq_weights, + consistency_anchors, consistency_weight)); + }else{ + RUN(kalign_run_seeded(copy, n_threads, run_type, + run_gpo[k], run_gpe[k], run_tgpe[k], + refine, 0, + run_seed, run_noise[k], + 0.0f, vsm_amax, + use_seq_weights, + consistency_anchors, consistency_weight)); + } + + char** aln_seqs = NULL; + MMALLOC(aln_seqs, sizeof(char*) * numseq); + for(int i = 0; i < numseq; i++){ + aln_seqs[i] = copy->sequences[i]->seq; + } + + RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); + RUN(extract_poars(poar, pm, k)); + + pos_matrix_free(pm); + pm = NULL; + MFREE(aln_seqs); + + alignments[k] = copy; + copy = NULL; + } + + RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, + &scores, &best_k)); + + if(!msa->quiet){ + LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); + } + + if(min_support > 0){ + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa)); + use_consensus = 1; + if(!msa->quiet){ + LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); + } + }else{ + double consensus_score = 0.0; + int min_sup = (n_runs + 2) / 3; + if(min_sup < 2) min_sup = 2; + + RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, + &consensus_msa)); + + RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, + &consensus_score)); + + if(!msa->quiet){ + LOG_MSG(" Consensus score: %.1f (selection: %.1f)", + consensus_score, scores[best_k]); + } + + if(consensus_score > scores[best_k]){ + use_consensus = 1; + if(!msa->quiet){ + LOG_MSG(" Using consensus alignment"); + } + }else{ + kalign_free_msa(consensus_msa); + consensus_msa = NULL; + if(!msa->quiet){ + LOG_MSG(" Keeping selection winner"); + } + } + } + + /* Post-selection refinement */ + if(!use_consensus){ + int ref_type = (run_types != NULL) ? run_types[best_k] : type; + uint64_t ref_seed = seed + (uint64_t)best_k; + + copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = 1; + + if(!msa->quiet){ + LOG_MSG(" Refining run %d...", best_k + 1); + } + + RUN(kalign_run_seeded(copy, n_threads, ref_type, + run_gpo[best_k], run_gpe[best_k], run_tgpe[best_k], + KALIGN_REFINE_CONFIDENT, 0, + ref_seed, run_noise[best_k], + 0.0f, vsm_amax, + use_seq_weights, + consistency_anchors, consistency_weight)); + + double refined_score = 0.0; + RUN(score_single_msa(copy, poar, numseq, n_runs, + &refined_score)); + + if(!msa->quiet){ + LOG_MSG(" Refined score: %.1f (was %.1f)", + refined_score, scores[best_k]); + } + + if(refined_score > scores[best_k]){ + kalign_free_msa(alignments[best_k]); + alignments[best_k] = copy; + copy = NULL; + if(!msa->quiet){ + LOG_MSG(" Using refined alignment"); + } + }else{ + kalign_free_msa(copy); + copy = NULL; + if(!msa->quiet){ + LOG_MSG(" Keeping original alignment"); + } + } + } + + MFREE(scores); + scores = NULL; + + if(use_consensus){ + RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); + kalign_free_msa(consensus_msa); + consensus_msa = NULL; + }else{ + RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); + } + + RUN(compute_residue_confidence(poar, msa)); + RUN(msa_sort_rank(msa)); + + STOP_TIMER(t_ensemble); + if(!msa->quiet){ + GET_TIMING(t_ensemble); + } + DESTROY_TIMER(t_ensemble); + + for(k = 0; k < n_runs; k++){ + if(alignments[k]) kalign_free_msa(alignments[k]); + } + MFREE(alignments); + poar_table_free(poar); + return OK; +ERROR: + if(copy) kalign_free_msa(copy); + if(consensus_msa) kalign_free_msa(consensus_msa); + if(pm) pos_matrix_free(pm); + if(alignments){ + for(k = 0; k < n_runs; k++){ + if(alignments[k]) kalign_free_msa(alignments[k]); + } + MFREE(alignments); + } + poar_table_free(poar); + if(scores) MFREE(scores); + return FAIL; +} + +/* ======================================================================== */ +/* kalign_generate_ensemble_runs: expand base config into N diversified runs. + * + * IMPORTANT: base.gpo/gpe/tgpe must be resolved (non-sentinel) values. + * If they are -1.0 (sentinel), the scale factors will produce garbage. + * The caller should resolve sentinels via aln_param_init before calling this. + */ +int kalign_generate_ensemble_runs(const struct kalign_run_config* base, + int n_runs, uint64_t seed, + struct kalign_run_config* out) +{ + int k; + + ASSERT(base != NULL, "base config is NULL"); + ASSERT(out != NULL, "output array is NULL"); + ASSERT(n_runs >= 1, "n_runs must be >= 1"); + + for(k = 0; k < n_runs; k++){ + /* Start with a copy of the base config */ + out[k] = *base; + + if(k == 0){ + /* Run 0: base params, deterministic tree */ + out[k].tree_seed = 0; + out[k].tree_noise = 0.0f; + }else{ + /* Apply diversity table scale factors */ + struct ensemble_params ep = run_params[k % N_RUN_PARAMS]; + out[k].gpo = base->gpo * ep.gpo_scale; + out[k].gpe = base->gpe * ep.gpe_scale; + out[k].tgpe = base->tgpe * ep.tgpe_scale; + out[k].tree_seed = seed + (uint64_t)k; + out[k].tree_noise = ep.noise; + } + } + + return OK; +ERROR: + return FAIL; +} + +/* ======================================================================== */ +/* kalign_ensemble_from_configs: run ensemble alignment with per-run configs. + * + * This is the core ensemble implementation used by kalign_align_full. + * Each runs[k] is a fully-specified run configuration. + */ +int kalign_ensemble_from_configs(struct msa* msa, + const struct kalign_run_config* runs, + int n_runs, + const struct kalign_ensemble_config* ens, + int n_threads) +{ + struct msa* copy = NULL; + struct msa* consensus_msa = NULL; + struct msa** alignments = NULL; + struct poar_table* poar = NULL; + struct pos_matrix* pm = NULL; + double* scores = NULL; + int numseq; + int k; + int best_k = 0; + int use_consensus = 0; + + ASSERT(msa != NULL, "No MSA"); + ASSERT(runs != NULL, "No run configs"); + ASSERT(n_runs >= 1, "n_runs must be >= 1"); + + RUN(kalign_essential_input_check(msa, 0)); + + numseq = msa->numseq; + + DECLARE_TIMER(t_ensemble); + if(!msa->quiet){ + LOG_MSG("Ensemble alignment with %d runs", n_runs); + } + START_TIMER(t_ensemble); + + if(msa->biotype == ALN_BIOTYPE_UNDEF){ + RUN(detect_alphabet(msa)); + } + + RUN(poar_table_alloc(&poar, numseq)); + MMALLOC(alignments, sizeof(struct msa*) * n_runs); + for(k = 0; k < n_runs; k++){ + alignments[k] = NULL; + } + + /* Run N alignments */ + for(k = 0; k < n_runs; k++){ + copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = 1; + + if(!msa->quiet){ + LOG_MSG(" Run %d/%d (gpo=%.1f gpe=%.1f tgpe=%.1f noise=%.2f)", + k + 1, n_runs, + runs[k].gpo, runs[k].gpe, runs[k].tgpe, + runs[k].tree_noise); + } + + if(runs[k].realign > 0){ + RUN(kalign_run_realign(copy, n_threads, runs[k].type, + runs[k].gpo, runs[k].gpe, runs[k].tgpe, + runs[k].refine, 0, + runs[k].dist_scale, runs[k].vsm_amax, + runs[k].realign, runs[k].use_seq_weights, + runs[k].consistency_anchors, + runs[k].consistency_weight)); + }else{ + RUN(kalign_run_seeded(copy, n_threads, runs[k].type, + runs[k].gpo, runs[k].gpe, runs[k].tgpe, + runs[k].refine, 0, + runs[k].tree_seed, runs[k].tree_noise, + runs[k].dist_scale, runs[k].vsm_amax, + runs[k].use_seq_weights, + runs[k].consistency_anchors, + runs[k].consistency_weight)); + } + + /* Extract POARs from the finalized alignment */ + char** aln_seqs = NULL; + MMALLOC(aln_seqs, sizeof(char*) * numseq); + for(int i = 0; i < numseq; i++){ + aln_seqs[i] = copy->sequences[i]->seq; + } + + RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); + RUN(extract_poars(poar, pm, k)); + + pos_matrix_free(pm); + pm = NULL; + MFREE(aln_seqs); + + alignments[k] = copy; + copy = NULL; + } + + /* Score all alignments and select the best */ + RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, + &scores, &best_k)); + + if(!msa->quiet){ + LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); + } + + /* Save POAR table if requested */ + if(ens != NULL && ens->save_poar != NULL){ + RUN(poar_table_write(poar, ens->save_poar)); + if(!msa->quiet){ + LOG_MSG(" Saved POAR table to %s", ens->save_poar); + } + } + + /* Determine min_support */ + int min_support = (ens != NULL) ? ens->min_support : 0; + + if(min_support > 0){ + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa)); + use_consensus = 1; + if(!msa->quiet){ + LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); + } + }else{ + double consensus_score = 0.0; + int min_sup = (n_runs + 2) / 3; + if(min_sup < 2) min_sup = 2; + + RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, + &consensus_msa)); + + RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, + &consensus_score)); + + if(!msa->quiet){ + LOG_MSG(" Consensus score: %.1f (selection: %.1f)", + consensus_score, scores[best_k]); + } + + if(consensus_score > scores[best_k]){ + use_consensus = 1; + if(!msa->quiet){ + LOG_MSG(" Using consensus alignment"); + } + }else{ + kalign_free_msa(consensus_msa); + consensus_msa = NULL; + if(!msa->quiet){ + LOG_MSG(" Keeping selection winner"); + } + } + } + + /* Post-selection refinement: re-run the winner with REFINE_CONFIDENT */ + if(!use_consensus){ + copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = 1; + + if(!msa->quiet){ + LOG_MSG(" Refining run %d...", best_k + 1); + } + + /* Post-selection refinement always uses REFINE_CONFIDENT and + the winning run's parameters (matching old behavior). */ + RUN(kalign_run_seeded(copy, n_threads, runs[best_k].type, + runs[best_k].gpo, runs[best_k].gpe, + runs[best_k].tgpe, + KALIGN_REFINE_CONFIDENT, 0, + runs[best_k].tree_seed, runs[best_k].tree_noise, + runs[best_k].dist_scale, runs[best_k].vsm_amax, + runs[best_k].use_seq_weights, + runs[best_k].consistency_anchors, + runs[best_k].consistency_weight)); + + double refined_score = 0.0; + RUN(score_single_msa(copy, poar, numseq, n_runs, + &refined_score)); + + if(!msa->quiet){ + LOG_MSG(" Refined score: %.1f (was %.1f)", + refined_score, scores[best_k]); + } + + if(refined_score > scores[best_k]){ + kalign_free_msa(alignments[best_k]); + alignments[best_k] = copy; + copy = NULL; + if(!msa->quiet){ + LOG_MSG(" Using refined alignment"); + } + }else{ + kalign_free_msa(copy); + copy = NULL; + if(!msa->quiet){ + LOG_MSG(" Keeping original alignment"); + } + } + } + + MFREE(scores); + scores = NULL; + + /* Copy the winning alignment back into the original MSA */ + if(use_consensus){ + RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); + kalign_free_msa(consensus_msa); + consensus_msa = NULL; + }else{ + RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); + } + + RUN(compute_residue_confidence(poar, msa)); + RUN(msa_sort_rank(msa)); + + STOP_TIMER(t_ensemble); + if(!msa->quiet){ + GET_TIMING(t_ensemble); + } + DESTROY_TIMER(t_ensemble); + + for(k = 0; k < n_runs; k++){ + if(alignments[k]) kalign_free_msa(alignments[k]); + } + MFREE(alignments); + poar_table_free(poar); + return OK; +ERROR: + if(copy) kalign_free_msa(copy); + if(consensus_msa) kalign_free_msa(consensus_msa); + if(pm) pos_matrix_free(pm); + if(alignments){ + for(k = 0; k < n_runs; k++){ + if(alignments[k]) kalign_free_msa(alignments[k]); + } + MFREE(alignments); + } + poar_table_free(poar); + if(scores) MFREE(scores); + return FAIL; +} + +/* ======================================================================== */ + int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support) diff --git a/lib/src/ensemble.h b/lib/src/ensemble.h index 1cc475e..62741de 100644 --- a/lib/src/ensemble.h +++ b/lib/src/ensemble.h @@ -2,6 +2,7 @@ #define ENSEMBLE_H #include +#include #ifdef ENSEMBLE_IMPORT #define EXTERN @@ -23,6 +24,28 @@ EXTERN int kalign_ensemble(struct msa* msa, int n_threads, int type, int realign, float use_seq_weights, int consistency_anchors, float consistency_weight); +EXTERN int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, + int n_runs, + const float* run_gpo, + const float* run_gpe, + const float* run_tgpe, + const int* run_types, + const float* run_noise, + uint64_t seed, int min_support, + int refine, float vsm_amax, + int realign, float use_seq_weights, + int consistency_anchors, float consistency_weight); + +EXTERN int kalign_ensemble_from_configs(struct msa* msa, + const struct kalign_run_config* runs, + int n_runs, + const struct kalign_ensemble_config* ens, + int n_threads); + +EXTERN int kalign_generate_ensemble_runs(const struct kalign_run_config* base, + int n_runs, uint64_t seed, + struct kalign_run_config* out); + EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); diff --git a/lib/src/msa_cmp.c b/lib/src/msa_cmp.c index 23fba02..b4ca8ad 100644 --- a/lib/src/msa_cmp.c +++ b/lib/src/msa_cmp.c @@ -49,6 +49,14 @@ int kalign_msa_compare(struct msa *r, struct msa *t, float *score) if(t->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(t); } + + if(r->alnlen == 0 && r->numseq > 0){ + r->alnlen = r->sequences[0]->len; + } + if(t->alnlen == 0 && t->numseq > 0){ + t->alnlen = t->sequences[0]->len; + } + RUN(kalign_check_msa(r,1)); RUN(kalign_check_msa(t,1)); @@ -418,12 +426,27 @@ int kalign_msa_compare_detailed(struct msa *r, struct msa *t, if(t->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(t); } + + /* Handle references read from file that had no gaps: + detect_aligned() sets ALN_STATUS_UNKNOWN when all sequences are + the same length with no gap characters. In that case alnlen is + still 0 but the sequences are stored verbatim in seq and the + alignment length equals the sequence length. */ + if(r->alnlen == 0 && r->numseq > 0){ + r->alnlen = r->sequences[0]->len; + } + if(t->alnlen == 0 && t->numseq > 0){ + t->alnlen = t->sequences[0]->len; + } + RUN(kalign_check_msa(r, 1)); RUN(kalign_check_msa(t, 1)); kalign_sort_msa(r); kalign_sort_msa(t); + ASSERT(r->alnlen > 0, "Reference alignment has length 0"); + /* Build scored column mask from reference alignment */ MMALLOC(scored_cols, sizeof(int) * r->alnlen); for(int c = 0; c < r->alnlen; c++){ @@ -465,6 +488,14 @@ int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, if(t->aligned == ALN_STATUS_ALIGNED){ finalise_alignment(t); } + + if(r->alnlen == 0 && r->numseq > 0){ + r->alnlen = r->sequences[0]->len; + } + if(t->alnlen == 0 && t->numseq > 0){ + t->alnlen = t->sequences[0]->len; + } + RUN(kalign_check_msa(r, 1)); RUN(kalign_check_msa(t, 1)); diff --git a/pyproject.toml b/pyproject.toml index 4ca86ec..1ebcedd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ skbio = ["scikit-bio>=0.6.3"] io = ["biopython>=1.85"] # For I/O helper functions analysis = ["pandas>=2.3.0", "matplotlib>=3.9.4", "seaborn>=0.13.2"] all = ["biopython>=1.85", "scikit-bio>=0.6.3", "pandas>=2.3.0", "matplotlib>=3.9.4", "seaborn>=0.13.2"] -benchmark = ["dash>=2.14", "plotly>=5.18", "pandas>=2.0", "tqdm>=4.60"] +benchmark = ["dash>=2.14", "plotly>=5.18", "pandas>=2.0", "tqdm>=4.60", "pymoo>=0.6", "rich>=13.0", "kneed>=0.8"] # Development dependencies dev = [ diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index f84fba5..207ae58 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -68,8 +69,8 @@ static py::object extract_confidence(struct msa* msa_data, int numseq) { return result; } -// Shared alignment routing helper — selects the appropriate kalign function -// based on the combination of parameters provided. +// Shared alignment helper — builds kalign_run_config and calls kalign_align_full. +// All alignment paths go through the same function, eliminating routing bugs. static int run_alignment(struct msa* msa_data, int n_threads, int seq_type, float gap_open, float gap_extend, float terminal_gap_extend, int refine, int adaptive_budget, @@ -84,7 +85,28 @@ static int run_alignment(struct msa* msa_data, int n_threads, int seq_type, if (!load_poar.empty()) { return kalign_consensus_from_poar(msa_data, load_poar.c_str(), min_support > 0 ? min_support : 2); - } else if (ensemble > 0) { + } + + struct kalign_run_config base = kalign_run_config_defaults(); + base.type = seq_type; + base.gpo = gap_open; + base.gpe = gap_extend; + base.tgpe = terminal_gap_extend; + base.refine = refine; + base.adaptive_budget = adaptive_budget; + base.dist_scale = dist_scale; + base.vsm_amax = vsm_amax; + base.use_seq_weights = use_seq_weights; + base.consistency_anchors = consistency_anchors; + base.consistency_weight = consistency_weight; + base.realign = realign; + + if (ensemble > 0) { + /* Ensemble path: resolve base gap penalties, then generate diversified runs. + We need resolved gap penalties for the scale factors to work. + Use kalign_ensemble (old path) which handles sentinel resolution internally, + OR resolve here and use kalign_align_full. We use the old kalign_ensemble + to maintain exact backward compatibility. */ const char* save_path = save_poar.empty() ? nullptr : save_poar.c_str(); return kalign_ensemble(msa_data, n_threads, seq_type, ensemble, gap_open, gap_extend, terminal_gap_extend, @@ -92,15 +114,10 @@ static int run_alignment(struct msa* msa_data, int n_threads, int seq_type, refine, dist_scale, vsm_amax, realign, use_seq_weights, consistency_anchors, consistency_weight); - } else if (realign > 0) { - return kalign_run_realign(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, dist_scale, vsm_amax, realign, use_seq_weights, consistency_anchors, consistency_weight); - } else if (consistency_anchors > 0) { - return kalign_run_seeded(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, 0, 0.0f, dist_scale, vsm_amax, use_seq_weights, consistency_anchors, consistency_weight); - } else if (dist_scale > 0.0f || vsm_amax >= 0.0f || use_seq_weights >= 0.0f) { - return kalign_run_dist_scale(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget, dist_scale, vsm_amax, use_seq_weights); - } else { - return kalign_run(msa_data, n_threads, seq_type, gap_open, gap_extend, terminal_gap_extend, refine, adaptive_budget); } + + /* Single-run path: all params go through kalign_align_full */ + return kalign_align_full(msa_data, &base, 1, nullptr, n_threads); } // Main alignment function @@ -497,6 +514,86 @@ void align_file_to_file( } } +// Ensemble with per-run parameters — playground for optimization. +// Each run gets its own gap penalties, matrix type, and tree noise. +// Now uses kalign_align_full with per-run configs. +void ensemble_custom_file_to_file( + const std::string& input_file, + const std::string& output_file, + const std::vector& run_gpo, + const std::vector& run_gpe, + const std::vector& run_tgpe, + const std::vector& run_noise, + const std::vector& run_types = {}, + const std::string& format = "fasta", + int seq_type = KALIGN_TYPE_PROTEIN, + uint64_t seed = 42, + int min_support = 0, + int refine = KALIGN_REFINE_NONE, + float vsm_amax = -1.0f, + int realign = 0, + float seq_weights = -1.0f, + int n_threads = 1, + int consistency_anchors = 0, + float consistency_weight = 2.0f +) { + int n_runs = static_cast(run_gpo.size()); + if (n_runs < 1) { + throw std::invalid_argument("Must provide at least 1 run"); + } + if (static_cast(run_gpe.size()) != n_runs || + static_cast(run_tgpe.size()) != n_runs || + static_cast(run_noise.size()) != n_runs) { + throw std::invalid_argument("All per-run arrays must have the same length"); + } + if (!run_types.empty() && static_cast(run_types.size()) != n_runs) { + throw std::invalid_argument("run_types must be empty or same length as run_gpo"); + } + + struct msa* msa_data = nullptr; + int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); + if (result != 0 || !msa_data) { + throw std::runtime_error("Failed to read input file: " + input_file); + } + + /* Build per-run configs */ + std::vector runs(n_runs); + for (int k = 0; k < n_runs; k++) { + runs[k] = kalign_run_config_defaults(); + runs[k].type = (!run_types.empty()) ? run_types[k] : seq_type; + runs[k].gpo = run_gpo[k]; + runs[k].gpe = run_gpe[k]; + runs[k].tgpe = run_tgpe[k]; + runs[k].tree_seed = seed + static_cast(k); + runs[k].tree_noise = run_noise[k]; + runs[k].vsm_amax = vsm_amax; + runs[k].dist_scale = 0.0f; + runs[k].use_seq_weights = seq_weights; + runs[k].refine = refine; + runs[k].realign = realign; + runs[k].consistency_anchors = consistency_anchors; + runs[k].consistency_weight = consistency_weight; + } + + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + ens.seed = seed; + ens.min_support = min_support; + + result = kalign_align_full(msa_data, runs.data(), n_runs, &ens, n_threads); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Ensemble alignment failed with error code: " + std::to_string(result)); + } + + result = kalign_write_msa(msa_data, const_cast(output_file.c_str()), + const_cast(format.c_str())); + kalign_free_msa(msa_data); + + if (result != 0) { + throw std::runtime_error("Failed to write output file: " + output_file); + } +} + PYBIND11_MODULE(_core, m) { m.doc() = "Python bindings for Kalign multiple sequence alignment"; @@ -723,6 +820,51 @@ PYBIND11_MODULE(_core, m) { Number of threads (default: 1) )pbdoc"); + // Ensemble with per-run parameters (optimization playground) + m.def("ensemble_custom_file_to_file", &ensemble_custom_file_to_file, + py::arg("input_file"), + py::arg("output_file"), + py::arg("run_gpo"), + py::arg("run_gpe"), + py::arg("run_tgpe"), + py::arg("run_noise"), + py::arg("run_types") = std::vector{}, + py::arg("format") = "fasta", + py::arg("seq_type") = KALIGN_TYPE_PROTEIN, + py::arg("seed") = (uint64_t)42, + py::arg("min_support") = 0, + py::arg("refine") = KALIGN_REFINE_NONE, + py::arg("vsm_amax") = -1.0f, + py::arg("realign") = 0, + py::arg("seq_weights") = -1.0f, + py::arg("n_threads") = 1, + py::arg("consistency_anchors") = 0, + py::arg("consistency_weight") = 2.0f, + R"pbdoc( + Ensemble alignment with per-run parameters. + + Each run gets its own gap penalties, matrix type, and tree noise. + This is a playground for optimizing ensemble configurations. + + Parameters + ---------- + input_file : str + Path to input sequence file + output_file : str + Path to output alignment file + run_gpo : list of float + Per-run gap open penalties (length = n_runs) + run_gpe : list of float + Per-run gap extend penalties + run_tgpe : list of float + Per-run terminal gap extend penalties + run_noise : list of float + Per-run tree noise sigma values + run_types : list of int, optional + Per-run matrix types (e.g. PROTEIN_PFASUM43, PROTEIN_PFASUM60, PROTEIN). + Empty = use seq_type for all runs. + )pbdoc"); + // Constants for sequence types m.attr("DNA") = KALIGN_TYPE_DNA; m.attr("DNA_INTERNAL") = KALIGN_TYPE_DNA_INTERNAL; diff --git a/setup_server.sh b/setup_server.sh new file mode 100755 index 0000000..f8ea3f2 --- /dev/null +++ b/setup_server.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Setup script for running kalign parameter optimization on a Linux server. +# Usage: bash setup_server.sh +set -euo pipefail + +echo "=== Kalign optimization server setup ===" + +# Check basics +echo "Checking prerequisites..." +command -v cmake >/dev/null 2>&1 || { echo "ERROR: cmake not found. Install with: sudo apt install cmake (or module load cmake)"; exit 1; } +command -v gcc >/dev/null 2>&1 || command -v cc >/dev/null 2>&1 || { echo "ERROR: C compiler not found."; exit 1; } + +# Install uv if not present +if ! command -v uv >/dev/null 2>&1; then + echo "Installing uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" +fi + +echo "uv version: $(uv --version)" + +# Build C library + Python extension +echo "" +echo "=== Building kalign Python package ===" +uv pip install -e ".[dev]" 2>/dev/null || uv pip install -e . + +# Install optimizer dependencies +echo "" +echo "=== Installing optimizer dependencies ===" +uv pip install pymoo rich + +# Quick smoke test +echo "" +echo "=== Smoke test ===" +uv run python -c " +import kalign +print(f'kalign version: {kalign.__version__}') +result = kalign.align(['ACDEFGHIK', 'ACDGHIK', 'ACDEFHIK']) +print(f'Alignment test: OK ({len(result)} sequences)') +" + +# Show system info +echo "" +echo "=== System info ===" +echo "CPU: $(nproc) threads available" +echo "RAM: $(free -h 2>/dev/null | awk '/^Mem:/{print $2}' || echo 'unknown')" +echo "Python: $(uv run python --version)" + +echo "" +echo "=== Ready! ===" +echo "" +echo "Example runs:" +echo "" +echo " # Quick test (10 minutes):" +echo " uv run python -m benchmarks.optimize_params --pop-size 20 --n-gen 5 --n-workers 8 --n-threads 1" +echo "" +echo " # Single-run optimization (~2-3 hours):" +echo " uv run python -m benchmarks.optimize_params --pop-size 60 --n-gen 50 --n-workers 56 --n-threads 1" +echo "" +echo " # Resume after interrupt:" +echo " uv run python -m benchmarks.optimize_params --resume benchmarks/results/optim/gen_checkpoint.pkl --n-gen 80 --n-workers 56 --n-threads 1" diff --git a/src/run_kalign.c b/src/run_kalign.c index c25060d..81b9d83 100644 --- a/src/run_kalign.c +++ b/src/run_kalign.c @@ -425,6 +425,8 @@ int run_kalign(struct parameters* param) param->load_poar, param->min_support > 0 ? param->min_support : 2)); }else if(param->ensemble > 0){ + /* Ensemble uses the old kalign_ensemble which handles + sentinel resolution + diversity table internally. */ RUN(kalign_ensemble(msa, param->nthreads, param->type, @@ -438,29 +440,21 @@ int run_kalign(struct parameters* param) param->refine, 0.0f, param->vsm_amax, param->realign, -1.0f, param->consistency_anchors, param->consistency_weight)); - }else if(param->realign > 0){ - RUN(kalign_run_realign(msa, - param->nthreads, - param->type, - param->gpo, - param->gpe, - param->tgpe, - param->refine, - param->adaptive_budget, - 0.0f, param->vsm_amax, - param->realign, -1.0f, - param->consistency_anchors, param->consistency_weight)); }else{ - RUN(kalign_run_seeded(msa, - param->nthreads, - param->type, - param->gpo, - param->gpe, - param->tgpe, - param->refine, - param->adaptive_budget, - 0, 0.0f, 0.0f, param->vsm_amax, -1.0f, - param->consistency_anchors, param->consistency_weight)); + /* Single-run: use kalign_align_full */ + struct kalign_run_config run = kalign_run_config_defaults(); + run.type = param->type; + run.gpo = param->gpo; + run.gpe = param->gpe; + run.tgpe = param->tgpe; + run.refine = param->refine; + run.adaptive_budget = param->adaptive_budget; + run.vsm_amax = param->vsm_amax; + run.use_seq_weights = -1.0f; + run.consistency_anchors = param->consistency_anchors; + run.consistency_weight = param->consistency_weight; + run.realign = param->realign; + RUN(kalign_align_full(msa, &run, 1, NULL, param->nthreads)); } diff --git a/uv.lock b/uv.lock index 94e9141..b875f8a 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,15 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[[package]] +name = "about-time" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3f/ccb16bdc53ebb81c1bf837c1ee4b5b0b69584fd2e4a802a2a79936691c0a/about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece", size = 15380, upload-time = "2022-12-21T04:15:54.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/cd/7ee00d6aa023b1d0551da0da5fee3bc23c3eeea632fbfc5126d1fec52b7e/about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341", size = 13295, upload-time = "2022-12-21T04:15:53.613Z" }, +] + [[package]] name = "alabaster" version = "0.7.16" @@ -48,6 +57,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "alive-progress" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "about-time" }, + { name = "graphemeu" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/26/d43128764a6f8fe1668c4f87aba6b1fe52bea81d05a35c84a70d3c70b6f7/alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c", size = 116281, upload-time = "2025-07-20T02:10:39.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/85/ec72f6c885703d18f3b09769645e950e14c7d0cc0a0e35d94127983f666f/alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff", size = 78403, upload-time = "2025-07-20T02:10:37.318Z" }, +] + [[package]] name = "array-api-compat" version = "1.11.2" @@ -81,6 +103,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/5d/493b1b5528ab5072feae30821ff3a07b7a0474213d548efb1fdf135f85c1/array_api_compat-1.13.0-py3-none-any.whl", hash = "sha256:c15026a0ddec42815383f07da285472e1b1ff2e632eb7afbcfe9b08fcbad9bf1", size = 58585, upload-time = "2025-12-28T11:26:56.081Z" }, ] +[[package]] +name = "autograd" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146, upload-time = "2025-05-05T12:49:02.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ea/e16f0c423f7d83cf8b79cae9452040fb7b2e020c7439a167ee7c317de448/autograd-1.8.0-py3-none-any.whl", hash = "sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78", size = 51478, upload-time = "2025-05-05T12:49:00.585Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -403,46 +439,93 @@ version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, - { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.10.*' and implementation_name != 'PyPy' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and implementation_name != 'PyPy' and sys_platform == 'win32') or (python_full_version >= '3.10' and implementation_name != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and 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" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, ] [[package]] @@ -589,6 +672,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "cma" +version = "4.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/ac/8c27720838e293898671f01b5c452236a0c74f4799a3f2d5fcccbbf50d71/cma-4.4.4.tar.gz", hash = "sha256:632bd654b5dce04c0eaa3166679d3e4773ce7a79eab7934e7f363c341b9a8170", size = 316645, upload-time = "2026-02-25T22:18:16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d4/ec46cedab6a6145e21768baa8110db3e2e836a320d8499e4ef18bc894e61/cma-4.4.4-py3-none-any.whl", hash = "sha256:edb6d02eb2aac2d54650f16a8f0c70711ff17445957de7c9de92ff7fd4b7ef38", size = 328311, upload-time = "2026-02-25T22:18:09.602Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -1155,6 +1252,27 @@ 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 = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -1379,6 +1497,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "graphemeu" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/20/d012f71e7d00e0d5bb25176b9a96c5313d0e30cf947153bfdfa78089f4bb/graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8", size = 307626, upload-time = "2025-01-15T09:48:59.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/18/36503ea63e1ecd0a95590d7b6b8b7d227a1e4541a154e1612a231def1bdc/graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542", size = 22670, upload-time = "2025-01-15T09:48:57.241Z" }, +] + [[package]] name = "h5py" version = "3.14.0" @@ -1665,9 +1792,13 @@ analysis = [ ] benchmark = [ { name = "dash" }, + { name = "kneed" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "plotly" }, + { name = "pymoo", version = "0.6.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pymoo", version = "0.6.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, { name = "tqdm" }, ] biopython = [ @@ -1737,6 +1868,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'" }, { name = "dash", marker = "extra == 'benchmark'", specifier = ">=2.14" }, { name = "flake8", marker = "extra == 'dev'" }, + { name = "kneed", marker = "extra == 'benchmark'", specifier = ">=0.8" }, { name = "matplotlib", marker = "extra == 'all'", specifier = ">=3.9.4" }, { name = "matplotlib", marker = "extra == 'analysis'", specifier = ">=3.9.4" }, { name = "mypy", marker = "extra == 'dev'" }, @@ -1746,6 +1878,7 @@ requires-dist = [ { name = "pandas", marker = "extra == 'analysis'", specifier = ">=2.3.0" }, { name = "pandas", marker = "extra == 'benchmark'", specifier = ">=2.0" }, { name = "plotly", marker = "extra == 'benchmark'", specifier = ">=5.18" }, + { name = "pymoo", marker = "extra == 'benchmark'", specifier = ">=0.6" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=6.0" }, { name = "pytest-benchmark", marker = "extra == 'dev'" }, @@ -1754,6 +1887,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'test'" }, { name = "pytest-xdist", marker = "extra == 'dev'" }, { name = "pytest-xdist", marker = "extra == 'test'" }, + { name = "rich", marker = "extra == 'benchmark'", specifier = ">=13.0" }, { name = "rich", marker = "extra == 'dev'" }, { name = "rich", marker = "extra == 'test'" }, { name = "scikit-bio", marker = "extra == 'all'", specifier = ">=0.6.3" }, @@ -2017,6 +2151,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] +[[package]] +name = "kneed" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/0f/958e27a378042e0366dfea8baab4a53121cb37c114117666051390cd7bb8/kneed-0.8.5.tar.gz", hash = "sha256:a4847ac4f1d04852fea278d5de7aa8bfdc3beb7fbca4a182fec0f0efee43f4b1", size = 12783, upload-time = "2023-07-09T01:51:08.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/1b/7e726d8616e813007874468c61790099ba21493e0ea07561b7d9fc53151c/kneed-0.8.5-py3-none-any.whl", hash = "sha256:2f3fbd4e9bd808e65052841448702c41ea64d5fc78735cbfc97ab25f08bd9815", size = 10290, upload-time = "2023-07-09T01:51:07.548Z" }, +] + [[package]] name = "librt" version = "0.7.8" @@ -2443,6 +2594,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "moocore" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/34/19341fe4ee06a82bf364fa7ac0998ec0cc67750133b55de3564312971116/moocore-0.2.0.tar.gz", hash = "sha256:3dc601f85f9a4743ed50ddd027dca30e3bb55c899916a092c2ece495b1b2de08", size = 404160, upload-time = "2026-01-11T11:43:18.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/aa/25a060b31e3b2f54d63f78331a324875c08226e1841da43ec3f371cf8e17/moocore-0.2.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:653449231f328d3c9e69693ec3d44e8c77f38ab7e9ef0c69dd9ded40449e980d", size = 565572, upload-time = "2026-01-11T11:43:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0d/a37abf346507a81554e43c8cfaedceac4776d8d29803a86e32d9eecfaafb/moocore-0.2.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8f091a7304532ed605acd82acd051e89af22ece8e2a27a3cee0faf9f2ea185", size = 743600, upload-time = "2026-01-11T11:43:08.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/aa/30e081884a653ab7f4a6bd9da99824c5d641c369928cee3cac4a7d801f36/moocore-0.2.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e93c07062adefd0fcba73a521f325f7fb874f2af92aaeec203cf9db31a41894b", size = 731717, upload-time = "2026-01-11T11:43:11.485Z" }, + { url = "https://files.pythonhosted.org/packages/52/9f/31d86de8a3bc21100b8a8b4c56d7d5a93f78e3c32c3c4a3395d985eb6baf/moocore-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90c7bde2164f9b95c6b2e870f0ca6ccc5dabff2bf8086162d7318c770e5868f", size = 737006, upload-time = "2026-01-11T11:43:13.024Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/79f0a968595c1d8a804fa9c66aa21cf676ffa9aacedf2ea50bd35d7f83d6/moocore-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d0699b770b5eebdeac477a356d539efa8807c6cf067a453a0682e0df2299a512", size = 724847, upload-time = "2026-01-11T11:43:14.275Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/535c1d448dbfa8ee79a83c36a6ded5a54ea35d02fac1fe2907ae540e369b/moocore-0.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:ea057409731e73dbc4ba4214cbf7747309695b01314f8786678b758cc9c561c4", size = 485644, upload-time = "2026-01-11T11:43:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/29bdef14758bfe869b74a0eae94d1a1ce220f75ce26ed6e7f4965ca70b49/moocore-0.2.0-cp310-abi3-win_arm64.whl", hash = "sha256:a7683feddfd2a47b4a0f89ee8d370cae72331792f68e67f083ccb37bb2f1c8cf", size = 476178, upload-time = "2026-01-11T11:43:16.919Z" }, +] + [[package]] name = "more-itertools" version = "10.8.0" @@ -3344,7 +3516,13 @@ name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", @@ -3372,6 +3550,119 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pymoo" +version = "0.6.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alive-progress", marker = "python_full_version < '3.10'" }, + { name = "autograd", marker = "python_full_version < '3.10'" }, + { name = "cma", marker = "python_full_version < '3.10'" }, + { name = "deprecated", marker = "python_full_version < '3.10'" }, + { name = "dill", marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/ed/ec5a76bb1556b774a67806c08234dab0e603509846b6b94934da59e5f4bd/pymoo-0.6.1.5.tar.gz", hash = "sha256:9ce71eaceb2f5cccf8c5af53102cf6d96fa911452addaf48fb971a60621f8364", size = 258027, upload-time = "2025-05-26T21:59:31.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/69/4a8c724ff65654cea9b7c6532ed271dcef60171f62cae01a407259e7b16c/pymoo-0.6.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00b5aec75e1ebdb13f537a45b17a33a8fd7ea7437b9bcfdc1a72e56ddf26dd7a", size = 1560163, upload-time = "2025-05-26T21:58:21.197Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/ef5c9d25c838a37fc5e612dcb1777580e646522e6896d8dd7354d858c867/pymoo-0.6.1.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdfe6b82582831a57bc441bb078b2dfa10654b04dc47208e714e2312c123cb4", size = 945392, upload-time = "2025-05-26T21:58:23.709Z" }, + { url = "https://files.pythonhosted.org/packages/fa/47/d2f52c59f1972279b030362f5a88d2de51c3936025e0d639eb37b6618bcf/pymoo-0.6.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48c0f930f344c2fd2fd82d7b30dd7e7d2613437c9519c2aaee73cfca7707fc82", size = 4213223, upload-time = "2025-05-26T21:58:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/da5881ecd278f320e639eb12195c0aa5356b00c7b3066e3fc6a9b0fa4d65/pymoo-0.6.1.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fc9079a7d4d4e392028deb8821a13b77a4ff834727725dbf62cddf44fd0a6e98", size = 5638677, upload-time = "2025-05-26T21:58:29.919Z" }, + { url = "https://files.pythonhosted.org/packages/3b/72/be83c99f185a574f14c001ac57c2886be0f675ed330865035674a0f2575a/pymoo-0.6.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:874c7f7c6da71520c230aa0f7150c949854d68eef6f57de4d9b8bbd4bc9dfb69", size = 2006657, upload-time = "2025-05-26T21:58:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/473e1e813ffba82c2c77f5e90553bc56a8ff8f02ffac1a5c67debcf7126c/pymoo-0.6.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2340e9da2e09c423d47cfe553375134702598e43701dc1cbd14c71010b381666", size = 1557733, upload-time = "2025-05-26T21:58:34.782Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/7a519e8029d97647457a9e8c14807c04e7b8b31cc845dcd1de238c8d5761/pymoo-0.6.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0d0a1c349ae6973ea6e0cedf208c760d557be6aca0fb86aff6db155e263dfef", size = 943827, upload-time = "2025-05-26T21:58:36.904Z" }, + { url = "https://files.pythonhosted.org/packages/db/0e/51cd797554fda6d9fb930d6f8d98ef9f01344b7a682e0ab89eefce616fa6/pymoo-0.6.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fd159285c5637d5c68d758c3498f13f066635455e2b0f2b3a43b9d44704c75", size = 4409417, upload-time = "2025-05-26T21:58:39.904Z" }, + { url = "https://files.pythonhosted.org/packages/5e/63/c921a65be1afa3c5eae0de78e17a3dc592c21de64de50e6bfb7645543154/pymoo-0.6.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:04a40005d4c18e194a380e360162063577cc1c206d5ca40f0dd463168e0efaf1", size = 5831827, upload-time = "2025-05-26T21:58:44.807Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/16a954eed286790fba89738d64cac4840434e6d05883ad559a254d9e9659/pymoo-0.6.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2fde9e9b6ed21b743e466d7a2225cf4aa8fc81408fe104948e70fbb0f5fd53de", size = 2006610, upload-time = "2025-05-26T21:58:47.384Z" }, + { url = "https://files.pythonhosted.org/packages/42/23/45dddc4897e384534d059688576b02016af72270d7ff18b14816a30a4c30/pymoo-0.6.1.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31d9f9522337c6ddfa6fc7670daa9ce4c777c104283824c3e6a2c482d8cde5b8", size = 1564924, upload-time = "2025-05-26T21:58:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4e/5ffa473b30b7ab44b3bb3c4bd4b77d81c8975fb8bb17381275e106137838/pymoo-0.6.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57dd99fd7fff871d42289646ee7899f5f85535a74d4fefcca900a9dde1067c07", size = 944016, upload-time = "2025-05-26T21:58:51.399Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9b/27e7e1e858c1f01b78a3de7e137c68905c693f1b81cee9826ff8b6bf6b78/pymoo-0.6.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:247099da5cf52092529089cd2b69d6cb959db9081d88789d6d1155778f392041", size = 4382163, upload-time = "2025-05-26T21:58:54.76Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/2dd71f8b75cad7251843a9b04587417171ee64233e8d1ef86fa59d385c7d/pymoo-0.6.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db6e562fad10afcfb250de116f958d7606f9ccb95d9a6e84b1c26378384cd736", size = 5786773, upload-time = "2025-05-26T21:58:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6a/f6733edb12bf452ca21d90bda2b50e13ade23c76e3018e092234b361840c/pymoo-0.6.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:44a151f83b9e455cdf1a8d63383c378b871c44592b6314167a39be3694a2fb01", size = 2009815, upload-time = "2025-05-26T21:59:00.778Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5e/260d77d5d44ee276fca63c902a38dbfa5315b13db4a856f4b4ede5769754/pymoo-0.6.1.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9ddaeb66ce18d473cdfdfd70c7e63e1cd7cddf47879e79bca1f8eab379a74413", size = 1552512, upload-time = "2025-05-26T21:59:02.748Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3f/38538bb89e92eb10357bc2aa8dcefdfa25ec01b57a2b4cd419e704de3139/pymoo-0.6.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:875d06c0f0617ea73eaedb810cc25d55b40b7ddf77db23f59bca51a18eab5079", size = 937888, upload-time = "2025-05-26T21:59:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/4d/75/7a7e1ddea474ef3e0d1845c20e7792743a8b01850e2956a6f776dbf87f46/pymoo-0.6.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da0d2afe9fa6a94fbec3fe970fa9426309b668eefb1eb796f44bfa186cf2c5ad", size = 4331420, upload-time = "2025-05-26T21:59:08.601Z" }, + { url = "https://files.pythonhosted.org/packages/85/6a/85e26ad9b046e89a4a77bee7f0ed3b3d77ecb2e743727d09187473346719/pymoo-0.6.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0926f8ba84fc1e104b30ccdcf0dd5ed886be209f7de6d729fe115cdd3fdec084", size = 5775567, upload-time = "2025-05-26T21:59:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1f/479758d597229563bf9d2003911bdb0829f031da4d1577bf76ff61e5b704/pymoo-0.6.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:36543ab8690c9afb4a07c795f58018223394b86c5ba0ce6044f7f28c193dfacc", size = 2008812, upload-time = "2025-05-26T21:59:15.135Z" }, + { url = "https://files.pythonhosted.org/packages/69/ed/91fc387f71c5ec59a793156339650d6fd9e26688849710d85044160662f0/pymoo-0.6.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dd9e9a898536402e40ca014d01ba78ba78cf464f4a5efa4eca15c30856f815f5", size = 1568278, upload-time = "2025-05-26T21:59:17.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/e7460a80363a7efeaab1d51879625d5094a670cfc8db5a0132b54461fef5/pymoo-0.6.1.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:92379d2b0e0730822fdf95dcf331a097476bb9188b0fe7082b25e71c403ab2a0", size = 949569, upload-time = "2025-05-26T21:59:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/7d/13/d013504279621e801ef66273bade95504d9045fbfd534c825c93a67e43cb/pymoo-0.6.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1274cabeb247ff1238479a60080ebc84a7003da0b6c0b44ab8dc2191717d6a79", size = 4219603, upload-time = "2025-05-26T21:59:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/85/dc/9438b12ffe64fc16d63e168fa1d62db0ba05af110fcb20000d52181d9542/pymoo-0.6.1.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d8c9fdaf9eca6c5abb961dd6bb763a7e751e4a142aea3d234414f0cc954e70b", size = 5652607, upload-time = "2025-05-26T21:59:26.513Z" }, + { url = "https://files.pythonhosted.org/packages/86/87/2dcb766f00c4c910d7c9375c9098bae0dc06a7704be04d4d30c107740a04/pymoo-0.6.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:54e0e1a448bc967db73dfa46a6c7a4daf1f9c5570e1cf0e8ca4b713ca6f14ea8", size = 2010532, upload-time = "2025-05-26T21:59:29.459Z" }, +] + +[[package]] +name = "pymoo" +version = "0.6.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alive-progress", marker = "python_full_version >= '3.10'" }, + { name = "autograd", marker = "python_full_version >= '3.10'" }, + { name = "cma", marker = "python_full_version >= '3.10'" }, + { name = "deprecated", marker = "python_full_version >= '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "moocore", marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.2", 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.10.*'" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/6c/5637688bb0e484ad7cd84e9f24f575d1b3c4ef28ce0974836bce7660106a/pymoo-0.6.1.6.tar.gz", hash = "sha256:d48077c7b612b149e7db5351459bf96a0950e84ebcd5b7b953bf46b3dcf1ac28", size = 1216128, upload-time = "2025-11-25T03:18:30.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/ec/091de8458b96421030aadfa10b0e4c25bd75d1c7123d1b24d76d8f7d902c/pymoo-0.6.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2d48fa7f8f8ea314405ad49a6b7f3dcf8f12c2024e319e2d47ede0937f8b39b", size = 2408116, upload-time = "2025-11-25T03:16:40.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ce/cbbb604855fd9d8acce7f677f0c8d56b26722bae8b6ed1f71cf0226d1a85/pymoo-0.6.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c8cfd085b74b3f673378109d8d0734d0cab460e3e78c09f691120ac0c2698aad", size = 1874607, upload-time = "2025-11-25T03:16:43.279Z" }, + { url = "https://files.pythonhosted.org/packages/52/39/ddbb1aab87dd8d43fb0ef996697f71bdfe9c1b1d80431dffc843137cf7f4/pymoo-0.6.1.6-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a9cde6ff7cc8497458166e2c14734bf4879c76256412cce0de2af86d2b71a8", size = 4963620, upload-time = "2025-11-25T03:16:47.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/31/1db5fb5184dd422c62084e68e72de2dccfeea37ef5764ec6c1c152637ef0/pymoo-0.6.1.6-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:007bdabe638d5cc1ff98d1df096e7cea42f266ecc7b84fc0aac695a20b6f02f6", size = 5033031, upload-time = "2025-11-25T03:16:50.849Z" }, + { url = "https://files.pythonhosted.org/packages/3e/39/a53c970509bef5cec678adca422029e3be3a3adbd9563d521498b5a443e1/pymoo-0.6.1.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62599637724f74820e16d813e6ca86af1733add4251e22b5a6c677df75dec06b", size = 5904102, upload-time = "2025-11-25T03:16:55.102Z" }, + { url = "https://files.pythonhosted.org/packages/11/3e/b76929def757beedf75deba214958af0a71548bab210b2cd1219265e4462/pymoo-0.6.1.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4f590fc62021d2583afdc88a2eb5ec878699786585f9ca82edf1fc32557ac96", size = 6088135, upload-time = "2025-11-25T03:16:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/ce483ad86ad9d767e33488f5b1cc39544b21121b481ce4f3c15eb1b1c3e8/pymoo-0.6.1.6-cp310-cp310-win_amd64.whl", hash = "sha256:3b9dcb6959cf2b12c0f1d4ac971fa53074430eb976367b2e7eec0d0301423f31", size = 1830499, upload-time = "2025-11-25T03:17:01.616Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/0abcc60bd65b2cac34e951d4bbe87514f142772b894de30e643efcd8f02f/pymoo-0.6.1.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0a42b6f3c26b2725b1edb1d4cfb98e5640bb0638813080ba1aa0daf23b2fd05", size = 2402920, upload-time = "2025-11-25T03:17:04.179Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/c252683f897e8a8bac00d8b16210327a631372fd2f8f7d1344f02c96947e/pymoo-0.6.1.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3e990dea0ad84fa2f89462215e34960579dbb9fef4d880aecb09f50cc18e713", size = 1871767, upload-time = "2025-11-25T03:17:06.368Z" }, + { url = "https://files.pythonhosted.org/packages/93/6d/f9772585189eec4990e7a39a6c87bb4202cbb89b92fe1800c58897a1b467/pymoo-0.6.1.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642308eae98c5b8aee96272e10c6409dd97289ef214ec7383b39425f01549b5f", size = 5120039, upload-time = "2025-11-25T03:17:10.424Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a3/b48789ba434f0015dc9dd14ab918e0fb0bd8bfb354d8c0b8c491d62255bc/pymoo-0.6.1.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50472dfb93b90b4801c63070a4afc786c52f7f56b345f345024b39cd35ff06c4", size = 5190633, upload-time = "2025-11-25T03:17:14.331Z" }, + { url = "https://files.pythonhosted.org/packages/df/37/59b1b0e1a1331692b63e0e019cc2b32ed22a33621e447b5aeccbf7b64151/pymoo-0.6.1.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:38fa5bf507cd7c07eae0286948db79f761b5b4c5d5956a31fd0e5a771c83fa47", size = 6061562, upload-time = "2025-11-25T03:17:18.037Z" }, + { url = "https://files.pythonhosted.org/packages/71/06/5f5ceb1daa38693ed5fd83c0f1568e69c7a9b7b65973ea81b6a248d31817/pymoo-0.6.1.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a68fb9d807c1cb6f36e13c24731c496b1e98b9c8810125072f19bd56baa6a4d5", size = 6247686, upload-time = "2025-11-25T03:17:22.399Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9e/250d5ca97f9474acafb0162695716bf94ba68fdbbb953f269a513ad8c30a/pymoo-0.6.1.6-cp311-cp311-win_amd64.whl", hash = "sha256:df9520fd7fab0761d5698dd36eabcfb18b5eb970ddf01e10eb30b764e4bb72af", size = 1829941, upload-time = "2025-11-25T03:17:24.898Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/9c6b9b5f18349dd797c0e7effe0dfd51402fa15e7cb293a525d6f73ffe03/pymoo-0.6.1.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fd4715b4015b10ad3cf02e5eb9abb57fe4bfa4d3378f9076b76d60c1ddfa46d6", size = 2417332, upload-time = "2025-11-25T03:17:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/cc8a39a4ea8e425dbc94db44f35f9e5dda3e1c0fbfdfba9fe9c58db600ab/pymoo-0.6.1.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f62fb1e2ba15f22ff5569c6d364dbf9dfe8b3722a114e6bc1df3cc10f8dd5044", size = 1876468, upload-time = "2025-11-25T03:17:29.407Z" }, + { url = "https://files.pythonhosted.org/packages/15/e8/c025f7330039cb320280b770a28f843174bddf9372bb9727add1031d21b7/pymoo-0.6.1.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd2b0fb19ce3e71a177ed3b965ce2d4334ffb6aad8f621c124de6dafa09b7758", size = 5086314, upload-time = "2025-11-25T03:17:32.513Z" }, + { url = "https://files.pythonhosted.org/packages/65/bb/402205166c567bb0b5452e2176dbf2af4b88d3577096f53fdc95be8ef562/pymoo-0.6.1.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533de86c284bf3b7b6da871e2ae07a914eaa5d3a51f5c4e6d7c055d3471c4467", size = 5181206, upload-time = "2025-11-25T03:17:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/ef60d8e7674906347dc9877afb4343fd7b21d0bdea60f736db3b857ea903/pymoo-0.6.1.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a2e9524b07baef135a7143700cc65a504c2123c717a6924fa939947b4fdba73", size = 6006960, upload-time = "2025-11-25T03:17:38.252Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ec/79ca5ca57d4d9e40691d3d0153675e4b1520278637c372380addc66a5823/pymoo-0.6.1.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:819a27ff19c146a64853a86ebab02bc6bbf28fb9fcf119e93cfc855ec6314f72", size = 6208775, upload-time = "2025-11-25T03:17:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/ac4f541e03fdd7e752c1e8ee766b1a6edcdff4d65930b3d593171eec02b3/pymoo-0.6.1.6-cp312-cp312-win_amd64.whl", hash = "sha256:5607e0359c96691158192bd3e2b939beab6d4ed25032ef4cb79edf588c2bb475", size = 1834431, upload-time = "2025-11-25T03:17:44.346Z" }, + { url = "https://files.pythonhosted.org/packages/7f/96/4e87b2330aaea8a23fe8fcfa3e1d7c60a57c4d968bdd330f2b9cbfeee7ea/pymoo-0.6.1.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:798e5af41527324303e56ac806f9ec34c65f31c2df9e904daeca68027bf91266", size = 2408764, upload-time = "2025-11-25T03:17:46.644Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/7b2ddc6f764b49a96a9653cc0f758cc773268d88b149ab889fdf42a5abfc/pymoo-0.6.1.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01e848762e04d183e8dcc12e1cfb1e2cdd2c5efeda47961dbcf6c86c09751e30", size = 1872399, upload-time = "2025-11-25T03:17:48.817Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/a70078ae29fe3363e06b0b3c761d9a53474d1fa25ce1f4a3a890ec3cc99a/pymoo-0.6.1.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:718b9e971e436c2ded520c4a6fe8c83b7f305da1fcc539d6386d025c6e9bfe66", size = 5066526, upload-time = "2025-11-25T03:17:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/89/94/7896e833bd29d45e5e721b0f71fdef9cf895e593ac28e7f83585218709ed/pymoo-0.6.1.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:981bd5b8581e959814a0e51982be1a964945fb7f599cfe7857a63cbc1ad97d58", size = 5176441, upload-time = "2025-11-25T03:17:55.925Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fe/6b8e2d82f118ef29fada61c0ac473b95cd0d5c742b0bad22bae178cde5c5/pymoo-0.6.1.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7c042120df86240c1451e2a1afb0f76b64acb8d736fb7687bc5e4aec40f7ff9e", size = 5984679, upload-time = "2025-11-25T03:17:59.917Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/5f1dd554fef9476642334dd923bc6943136f9d1af7dc97c304f0e3c1684e/pymoo-0.6.1.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5086f46020aa1a13a0e3142ef87b6487e91ea5d25b7b69467039f0d3f5a57cb2", size = 6201995, upload-time = "2025-11-25T03:18:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/287e3cae3b2bc3b387cbd1c8f0fecc5038190eef62d823f8b810dca43a95/pymoo-0.6.1.6-cp313-cp313-win_amd64.whl", hash = "sha256:c16e05b36e081d0ea6aee499346f65466811f63f5837f0d09de866624cb7355b", size = 1833324, upload-time = "2025-11-25T03:18:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/51/ec/f0f65e8abd724e62c9b5ebfcaa2280ed327edd33303d4acc8df1c832e07f/pymoo-0.6.1.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7d13e14434f0555fbeda20764d34be4dc94f53ae7040a337928d7bfe42d3422a", size = 2426044, upload-time = "2025-11-25T03:18:08.742Z" }, + { url = "https://files.pythonhosted.org/packages/dd/19/0113bd220b0e20834216a28fad49eac88c3770660d32aeca7ac29b1a382e/pymoo-0.6.1.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:466f88073c9f498bcf75334a2e47279968003d91b7fe01df1b056b5a6c3434d8", size = 1888556, upload-time = "2025-11-25T03:18:11.451Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/e2bba2460219be070316bd0f93a61ff72fc6ae3d14b964cf4e837acf0f5f/pymoo-0.6.1.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791188677b42e104c41cd1c65370a94fbbec756d8894947863f42a1f76352c90", size = 5057079, upload-time = "2025-11-25T03:18:16.47Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a4/7d7e423022ab7ca14893312cc9f41121a1c1edc197b1404ac0b489d9647b/pymoo-0.6.1.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9dd7da8b18bffac6b09c75b4a4a645232994d91fc0cdfe39baffdc08fa5d252", size = 5143465, upload-time = "2025-11-25T03:18:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/06/f1/115a03e18820d09107c1aae1029719bd5a42ed9712f8fcf5e6334f44fe8c/pymoo-0.6.1.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f41b359d6de6c9d99ab7da94f115c5aea4d21e74a252bef7c8debec8f20f0b9f", size = 5985143, upload-time = "2025-11-25T03:18:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/ac62bb650197df717bde976b5edf6e46856714900106df0d541f99f5aa0b/pymoo-0.6.1.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ecf1ce515e72d59b78c909aa142d2369cc3c6d771ac7c9162831c0b62a869c8f", size = 6188089, upload-time = "2025-11-25T03:18:27.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/bd/76eecf37a469a2e19c82c85b20402cbeece261ef9d9286189502f39e3566/pymoo-0.6.1.6-cp314-cp314-win_amd64.whl", hash = "sha256:56ecb6f9f5ac0559829e183a23ea3672eae8c1eb9d745305bef98da8d1614d28", size = 1851874, upload-time = "2025-11-25T03:18:28.969Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -4488,6 +4779,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/fe375f8a012e5f25b2cd31b093860c8c6540be445345c6f886e5d8bca9ef/wrapt-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5e0fa9cc32300daf9eb09a1f5bdc6deb9a79defd70d5356ba453bcd50aef3742", size = 60661, upload-time = "2026-03-06T02:54:06.572Z" }, + { url = "https://files.pythonhosted.org/packages/d8/2a/0dff969ddf4d3f69f051c8f81afbd3a9fc9fb08ab993b1061ee582b6543c/wrapt-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:710f6e5dfaf6a5d5c397d2d6758a78fecd9649deb21f1b645f5b57a328d63050", size = 61602, upload-time = "2026-03-06T02:53:44.48Z" }, + { url = "https://files.pythonhosted.org/packages/25/62/b80dd7a6c21486a7b8aea63b6bac509b2e4ea184b0eefe3795aa7202a92c/wrapt-2.1.2-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:305d8a1755116bfdad5dda9e771dcb2138990a1d66e9edd81658816edf51aed1", size = 113340, upload-time = "2026-03-06T02:54:44.626Z" }, + { url = "https://files.pythonhosted.org/packages/82/06/adbe093e07a775d8687cc45329cda9e1b33779357d146c688accbc3a9f1f/wrapt-2.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0d8fc30a43b5fe191cf2b1a0c82bab2571dadd38e7c0062ee87d6df858dd06e", size = 115305, upload-time = "2026-03-06T02:53:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/3f/dd/31c2596c6bf6bfb1874aa637c66e3028baa83d00708d1439db3b395f8371/wrapt-2.1.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5d516e22aedb7c9c1d47cba1c63160b1a6f61ec2f3948d127cd38d5cfbb556f", size = 111691, upload-time = "2026-03-06T02:53:17.845Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/e9ba179f4a00b7eb7ab8afc1f729fc3be8bd468b9f1d33be1fd99476493a/wrapt-2.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:45914e8efbe4b9d5102fcf0e8e2e3258b83a5d5fba9f8f7b6d15681e9d29ffe0", size = 114507, upload-time = "2026-03-06T02:54:49.398Z" }, + { url = "https://files.pythonhosted.org/packages/0f/dd/5ce1332e824503fb7041a8f8b51ec1f06e7033834e38c01416fa1c599668/wrapt-2.1.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:478282ebd3795a089154fb16d3db360e103aa13d3b2ad30f8f6aac0d2207de0e", size = 110945, upload-time = "2026-03-06T02:54:32.088Z" }, + { url = "https://files.pythonhosted.org/packages/1b/17/d1c1d7b63a029205fe8add19db654fd105e2a92a3776c1312e74456ce3ab/wrapt-2.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3756219045f73fb28c5d7662778e4156fbd06cf823c4d2d4b19f97305e52819c", size = 113107, upload-time = "2026-03-06T02:54:05.226Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/aa5b1570ca36a0533ad5fc9d9e436047b9af187f9bd182f5eb6b718fe28b/wrapt-2.1.2-cp39-cp39-win32.whl", hash = "sha256:b8aefb4dbb18d904b96827435a763fa42fc1f08ea096a391710407a60983ced8", size = 57984, upload-time = "2026-03-06T02:53:10.07Z" }, + { url = "https://files.pythonhosted.org/packages/71/3a/a0c92e4c8b6cd8ef179c62249f03f5ce50c142f71fe04c2a14279bd826b4/wrapt-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:e5aeab8fe15c3dff75cfee94260dcd9cded012d4ff06add036c28fae7718593b", size = 60334, upload-time = "2026-03-06T02:53:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/2725632aa7f1f70a9730952444e2ba856bd15ce8ee0210afcdb50f48ab69/wrapt-2.1.2-cp39-cp39-win_arm64.whl", hash = "sha256:f069e113743a21a3defac6677f000068ebb931639f789b5b226598e247a4c89e", size = 58759, upload-time = "2026-03-06T02:53:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + [[package]] name = "zipp" version = "3.23.0" From f0cf213d9333d851c6b8f9b4bd8be0ba28e7cc5e Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Thu, 12 Mar 2026 11:16:11 +0800 Subject: [PATCH 02/29] Prior to parameter cleanup --- .containerignore | 3 + benchmarks/optimize_unified.py | 16 +++- benchmarks/scoring.py | 40 ++++++---- lib/include/kalign/kalign.h | 21 ++++++ lib/src/aln_mem.c | 1 + lib/src/aln_param.c | 3 + lib/src/aln_param.h | 1 - lib/src/aln_run.c | 1 + lib/src/aln_wrap.c | 105 ++++++++++++++++++++++++++ lib/src/msa_check.c | 56 -------------- lib/src/msa_check.h | 1 - lib/src/msa_cmp.c | 111 ++++++++++++++++++++++++---- lib/src/msa_op.c | 3 +- lib/src/task.c | 6 ++ python-kalign/__init__.py | 131 +++++++++++++++++++++++++++------ python-kalign/_core.cpp | 69 +++++++++++++++++ 16 files changed, 459 insertions(+), 109 deletions(-) diff --git a/.containerignore b/.containerignore index 69ee40c..b476867 100644 --- a/.containerignore +++ b/.containerignore @@ -1,5 +1,8 @@ .git build +build-asan +build-debug +build-release benchmarks/data benchmarks/results __pycache__ diff --git a/benchmarks/optimize_unified.py b/benchmarks/optimize_unified.py index cf2a4b3..a8bdf67 100644 --- a/benchmarks/optimize_unified.py +++ b/benchmarks/optimize_unified.py @@ -46,8 +46,18 @@ --n-gen 150 --n-workers 56 """ -import argparse import os +# Must set OMP_NUM_THREADS before importing kalign (or any C extension that +# uses OpenMP). ProcessPoolExecutor uses fork(), and forked children inherit +# the parent's OpenMP thread-pool state — but the pool threads are dead in the +# child. If OpenMP later tries to use them, the child segfaults. Setting +# this to "1" prevents the parent from ever creating extra threads, making +# fork safe. The actual per-alignment thread count is controlled by the +# n_threads parameter passed to kalign at call time. +if "OMP_NUM_THREADS" not in os.environ: + os.environ["OMP_NUM_THREADS"] = "1" + +import argparse import pickle import signal import sys @@ -787,6 +797,8 @@ def on_gen_end(self, gen: int, pareto_front: List[dict]): def _eval_one_unified_fold(args_tuple): """Evaluate one (individual, fold) pair.""" ind_idx, fold_idx, x, test_cases, n_threads, max_runs = args_tuple + import faulthandler, sys + faulthandler.enable(file=sys.stderr) params = decode_unified_params(x, max_runs) result = evaluate_unified(params, test_cases, n_threads, quiet=True) return ind_idx, fold_idx, params, result @@ -795,6 +807,8 @@ def _eval_one_unified_fold(args_tuple): def _eval_baseline(args_tuple): """Evaluate one baseline configuration on a set of cases.""" name, fi, bl_params, test_cases, n_threads = args_tuple + import faulthandler, sys + faulthandler.enable(file=sys.stderr) result = evaluate_unified(bl_params, test_cases, n_threads, quiet=True) return name, fi, result diff --git a/benchmarks/scoring.py b/benchmarks/scoring.py index 62a4084..b4b1dab 100644 --- a/benchmarks/scoring.py +++ b/benchmarks/scoring.py @@ -38,19 +38,29 @@ def to_dict(self) -> dict: def align_with_python_api( case: BenchmarkCase, output: Path, n_threads: int = 1, refine: str = "none", adaptive_budget: bool = False, ensemble: int = 0, + mode: Optional[str] = None, ) -> float: """Align using kalign Python API. Returns wall time in seconds.""" start = time.perf_counter() - kalign.align_file_to_file( - str(case.unaligned), - str(output), - format="fasta", - seq_type=case.seq_type, - n_threads=n_threads, - refine=refine, - adaptive_budget=adaptive_budget, - ensemble=ensemble, - ) + if mode is not None: + kalign.align_file_to_file( + str(case.unaligned), + str(output), + format="fasta", + n_threads=n_threads, + mode=mode, + ) + else: + kalign.align_file_to_file( + str(case.unaligned), + str(output), + format="fasta", + seq_type=case.seq_type, + n_threads=n_threads, + refine=refine, + adaptive_budget=adaptive_budget, + ensemble=ensemble, + ) return time.perf_counter() - start @@ -193,6 +203,7 @@ def run_case( refine: str = "none", adaptive_budget: bool = False, ensemble: int = 0, + mode: Optional[str] = None, ) -> AlignmentResult: """Run alignment + scoring for a single benchmark case.""" with tempfile.TemporaryDirectory() as tmpdir: @@ -200,7 +211,10 @@ def run_case( try: if method == "python_api": - wall_time = align_with_python_api(case, output, n_threads, refine, adaptive_budget, ensemble) + wall_time = align_with_python_api( + case, output, n_threads, refine, adaptive_budget, + ensemble, mode=mode, + ) elif method == "cli": wall_time = align_with_cli(case, output, binary, n_threads, refine, adaptive_budget, ensemble) elif method in EXTERNAL_TOOLS: @@ -219,7 +233,7 @@ def run_case( sp_score=sp_score, wall_time=wall_time, seq_type=case.seq_type, - refine="n/a" if is_external else refine, + refine="n/a" if is_external else (mode or refine), ensemble=0 if is_external else ensemble, recall=detailed["recall"], precision=detailed["precision"], @@ -235,7 +249,7 @@ def run_case( sp_score=0.0, wall_time=0.0, seq_type=case.seq_type, - refine="n/a" if is_external else refine, + refine="n/a" if is_external else (mode or refine), ensemble=0 if is_external else ensemble, error=str(e), ) diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 09e115e..d9635ac 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -25,6 +25,7 @@ #define KALIGN_TYPE_PROTEIN_PFASUM60 6 #define KALIGN_TYPE_PROTEIN_PFASUM_AUTO 7 #define KALIGN_TYPE_UNDEFINED 8 +#define KALIGN_TYPE_PROTEIN_CORBLOSUM66 9 #define KALIGN_REFINE_NONE 0 #define KALIGN_REFINE_ALL 1 @@ -138,6 +139,26 @@ EXTERN int kalign_generate_ensemble_runs(const struct kalign_run_config* base, int n_runs, uint64_t seed, struct kalign_run_config* out); +/* Get a built-in mode preset (protein only). + * + * Presets were derived from NSGA-III multi-objective optimization + * (objectives: F1, TC, wall_time) with 5-fold cross-validation on + * BAliBASE v4. + * + * mode: "fast", "default", or "accurate" (case-insensitive). + * NULL is treated as "default". + * runs: caller-allocated array of at least KALIGN_MAX_PRESET_RUNS configs. + * n_runs: filled with the number of runs in the preset. + * ens: filled with ensemble config (only meaningful when *n_runs > 1). + * + * Returns 0 on success, -1 if mode is unknown. */ +#define KALIGN_MAX_PRESET_RUNS 8 + +EXTERN int kalign_get_mode_preset(const char *mode, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens); + #undef KALIGN_IMPORT #undef EXTERN diff --git a/lib/src/aln_mem.c b/lib/src/aln_mem.c index 415ccf4..7c94453 100644 --- a/lib/src/aln_mem.c +++ b/lib/src/aln_mem.c @@ -35,6 +35,7 @@ int alloc_aln_mem(struct aln_mem** mem, int x) m->flip_bit_map = NULL; m->flip_n_targets = 0; m->flip_n_uncertain = 0; + m->run_parallel = 0; m->ap = NULL; m->consistency = NULL; diff --git a/lib/src/aln_param.c b/lib/src/aln_param.c index b9f3b43..2c25d4b 100644 --- a/lib/src/aln_param.c +++ b/lib/src/aln_param.c @@ -65,6 +65,9 @@ int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int case KALIGN_TYPE_PROTEIN_PFASUM60: set_subm_gaps_PFASUM60(ap); break; + case KALIGN_TYPE_PROTEIN_CORBLOSUM66: + set_subm_gaps_CorBLOSUM66_13plus(ap); + break; case KALIGN_TYPE_DNA: ERROR_MSG("Detected protein sequences but --type dna option was selected."); break; diff --git a/lib/src/aln_param.h b/lib/src/aln_param.h index 25e654b..3d522c6 100644 --- a/lib/src/aln_param.h +++ b/lib/src/aln_param.h @@ -23,7 +23,6 @@ struct aln_param{ float gpo; float gpe; float tgpe; - float score; float dist_scale; /* distance-dependent gap scaling: 0=off, >0 scales gap penalties down for divergent pairs */ float vsm_amax; /* variable scoring matrix: 0=off, >0 subtracts a(d)=max(0,amax-d) from subm scores */ float subm_offset; /* computed per alignment step: amount to subtract from substitution scores */ diff --git a/lib/src/aln_run.c b/lib/src/aln_run.c index 45f6e6d..8744886 100644 --- a/lib/src/aln_run.c +++ b/lib/src/aln_run.c @@ -114,6 +114,7 @@ void recursive_aln(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, ui ml->ap = ap; ml->mode = ALN_MODE_FULL; + ml->run_parallel = msa->run_parallel; do_align(msa,t,ml,c); active[local_t->a] = 0; diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index 6e2468c..bea07d5 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -1,5 +1,6 @@ #include "tldevel.h" #include "tlmisc.h" +#include #include "esl_stopwatch.h" #include "task.h" #include "msa_struct.h" @@ -747,3 +748,107 @@ int kalign_align_full(struct msa* msa, ERROR: return FAIL; } + +/* ======================================================================== */ +/* NSGA-III optimized protein mode presets */ +/* ======================================================================== */ + +/* Helper: fill one run config with common preset values */ +static void preset_run(struct kalign_run_config *r, + int type, float gpo, float gpe, float tgpe, + float vsm_amax, float seq_weights, + int realign, int refine, + uint64_t seed, float noise) +{ + *r = kalign_run_config_defaults(); + r->type = type; + r->gpo = gpo; + r->gpe = gpe; + r->tgpe = tgpe; + r->vsm_amax = vsm_amax; + r->use_seq_weights = seq_weights; + r->realign = realign; + r->refine = refine; + r->consistency_anchors = 0; + r->consistency_weight = 1.0f; + r->tree_seed = seed; + r->tree_noise = noise; +} + +int kalign_get_mode_preset(const char *mode, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens) +{ + const char *m = mode ? mode : "default"; + + *ens = kalign_ensemble_config_defaults(); + + if(strcasecmp(m, "fast") == 0){ + *n_runs = 1; + preset_run(&runs[0], + KALIGN_TYPE_PROTEIN_PFASUM60, + 8.4087f, 0.5153f, 0.4927f, /* gpo, gpe, tgpe */ + 1.448f, /* vsm_amax */ + 1.063f, /* seq_weights */ + 0, KALIGN_REFINE_NONE, /* realign, refine */ + 42, 0.1623f); /* seed, noise */ + return 0; + } + + if(strcasecmp(m, "default") == 0){ + *n_runs = 5; + float vsm = 1.885f; + float sw = 0.592f; + int ra = 0; + int ref = KALIGN_REFINE_NONE; + + preset_run(&runs[0], KALIGN_TYPE_PROTEIN_DIVERGENT, + 9.5703f, 0.6206f, 1.5751f, + vsm, sw, ra, ref, 42, 0.063f); + preset_run(&runs[1], KALIGN_TYPE_PROTEIN_PFASUM43, + 5.6154f, 0.5469f, 1.0163f, + vsm, sw, ra, ref, 43, 0.2828f); + preset_run(&runs[2], KALIGN_TYPE_PROTEIN_DIVERGENT, + 4.8979f, 1.3657f, 1.2367f, + vsm, sw, ra, ref, 44, 0.4046f); + preset_run(&runs[3], KALIGN_TYPE_PROTEIN_DIVERGENT, + 7.244f, 0.9013f, 0.7332f, + vsm, sw, ra, ref, 45, 0.3067f); + preset_run(&runs[4], KALIGN_TYPE_PROTEIN_PFASUM43, + 8.4354f, 1.8028f, 0.919f, + vsm, sw, ra, ref, 46, 0.1964f); + + ens->min_support = 3; + return 0; + } + + if(strcasecmp(m, "accurate") == 0){ + *n_runs = 5; + float vsm = 1.682f; + float sw = 1.48f; + int ra = 2; + int ref = KALIGN_REFINE_NONE; + + preset_run(&runs[0], KALIGN_TYPE_PROTEIN_DIVERGENT, + 13.1073f, 0.6667f, 0.613f, + vsm, sw, ra, ref, 42, 0.3472f); + preset_run(&runs[1], KALIGN_TYPE_PROTEIN_PFASUM43, + 7.3036f, 0.6285f, 2.8521f, + vsm, sw, ra, ref, 43, 0.2264f); + preset_run(&runs[2], KALIGN_TYPE_PROTEIN_PFASUM43, + 2.2452f, 2.0447f, 0.5878f, + vsm, sw, ra, ref, 44, 0.1481f); + preset_run(&runs[3], KALIGN_TYPE_PROTEIN_PFASUM43, + 3.9617f, 0.8429f, 0.5156f, + vsm, sw, ra, ref, 45, 0.4338f); + preset_run(&runs[4], KALIGN_TYPE_PROTEIN_PFASUM43, + 7.5402f, 1.8516f, 0.8772f, + vsm, sw, ra, ref, 46, 0.1979f); + + ens->min_support = 3; + return 0; + } + + return -1; +} diff --git a/lib/src/msa_check.c b/lib/src/msa_check.c index fe49e50..3846f2f 100644 --- a/lib/src/msa_check.c +++ b/lib/src/msa_check.c @@ -22,47 +22,8 @@ struct sort_struct_name_chksum{ static int GCGchecksum(char *seq, int len); static int sort_by_name(const void *a, const void *b); static int sort_by_chksum(const void *a, const void *b); -static int sort_by_both(const void *a, const void *b); - int sort_seq_by_len(const void *a, const void *b); -int kalign_sort_msa(struct msa *msa) -{ - struct sort_struct_name_chksum** a = NULL; - - MMALLOC(a, sizeof(struct sort_struct_name_chksum *) * msa->numseq); - - for(int i = 0; i < msa->numseq;i++){ - a[i] = NULL; - MMALLOC(a[i], sizeof(struct sort_struct_name_chksum)); - a[i]->seq = msa->sequences[i]; - a[i]->name = &msa->sequences[i]->name; - a[i]->chksum = GCGchecksum(msa->sequences[i]->seq, msa->sequences[i]->len); - a[i]->action = 0; - } - - qsort(a, msa->numseq, sizeof(struct sort_struct*),sort_by_both); - - for(int i = 0; i < msa->numseq;i++){ - msa->sequences[i] = a[i]->seq; - } - - for(int i = 0; i < msa->numseq;i++){ - MFREE(a[i]); - } - MFREE(a); - return OK; -ERROR: - if(a){ - for(int i = 0; i < msa->numseq;i++){ - MFREE(a[i]); - } - MFREE(a); - } - return FAIL; -} - - int kalign_essential_input_check(struct msa *msa, int exit_on_error) { int problem_len0 = 0; @@ -244,23 +205,6 @@ int kalign_check_msa(struct msa* msa, int exit_on_error) return FAIL; } -int sort_by_both(const void *a, const void *b) -{ - struct sort_struct_name_chksum* const *one = a; - struct sort_struct_name_chksum* const *two = b; - - if(strncmp(*(*one)->name, *(*two)->name,MSA_NAME_LEN) < 0){ - return -1; - }else if(strncmp(*(*one)->name, *(*two)->name,MSA_NAME_LEN) == 0 ){ - if((*one)->chksum > (*two)->chksum){ - return -1; - }else{ - return 1; - } - }else{ - return 1; - } -} int sort_by_name(const void *a, const void *b) { struct sort_struct_name_chksum* const *one = a; diff --git a/lib/src/msa_check.h b/lib/src/msa_check.h index c381a54..39e61c9 100644 --- a/lib/src/msa_check.h +++ b/lib/src/msa_check.h @@ -15,7 +15,6 @@ struct msa; EXTERN int kalign_essential_input_check(struct msa *msa, int exit_on_error); EXTERN int kalign_check_msa(struct msa* msa, int exit_on_error); -EXTERN int kalign_sort_msa(struct msa *msa); #undef MSA_CHECK_IMPORT #undef EXTERN diff --git a/lib/src/msa_cmp.c b/lib/src/msa_cmp.c index b4ca8ad..9ff6558 100644 --- a/lib/src/msa_cmp.c +++ b/lib/src/msa_cmp.c @@ -1,6 +1,7 @@ #include "tldevel.h" #include +#include #include "msa_struct.h" #include "msa_check.h" #include "msa_op.h" @@ -26,6 +27,95 @@ struct detailed_pair_stats { int64_t common_all; /* all matches → for precision */ }; +/* Sort key for robust sequence matching in MSA comparison. + Key = name + '\0' + ungapped_sequence, so sequences are sorted first + by name, then by biological content. This guarantees identical ordering + of ref and test MSAs regardless of gap patterns or duplicate names. */ +struct cmp_sort_entry { + struct msa_seq* seq; + char* key; + int key_len; +}; + +static int cmp_sort_by_key(const void *a, const void *b) +{ + const struct cmp_sort_entry* const *ea = a; + const struct cmp_sort_entry* const *eb = b; + int min_len = (*ea)->key_len < (*eb)->key_len ? (*ea)->key_len : (*eb)->key_len; + int r = memcmp((*ea)->key, (*eb)->key, min_len); + if(r != 0) return r; + return (*ea)->key_len - (*eb)->key_len; +} + +/* Sort an MSA's sequences by name + ungapped sequence content. + Works on finalized alignments (seq->seq may contain gaps) and on + raw unaligned sequences alike. Both ref and test get identical + ordering because the sort key ignores gap characters entirely. */ +static int sort_msa_for_comparison(struct msa* msa) +{ + struct cmp_sort_entry** entries = NULL; + int i, j; + + MMALLOC(entries, sizeof(struct cmp_sort_entry*) * msa->numseq); + for(i = 0; i < msa->numseq; i++){ + entries[i] = NULL; + MMALLOC(entries[i], sizeof(struct cmp_sort_entry)); + entries[i]->seq = msa->sequences[i]; + entries[i]->key = NULL; + + /* Compute key length: name_len + 1 (separator) + ungapped_len */ + int name_len = strnlen(msa->sequences[i]->name, MSA_NAME_LEN); + int seq_len = 0; + /* Count ungapped characters — works whether seq has gaps or not */ + int scan_len = (msa->alnlen > 0) ? msa->alnlen : msa->sequences[i]->len; + for(j = 0; j < scan_len; j++){ + if(isalpha((int)msa->sequences[i]->seq[j])){ + seq_len++; + } + } + + entries[i]->key_len = name_len + 1 + seq_len; + MMALLOC(entries[i]->key, sizeof(char) * (entries[i]->key_len + 1)); + + /* Build key: name + '\0' separator + ungapped sequence */ + memcpy(entries[i]->key, msa->sequences[i]->name, name_len); + entries[i]->key[name_len] = '\0'; + int p = name_len + 1; + for(j = 0; j < scan_len; j++){ + char c = msa->sequences[i]->seq[j]; + if(isalpha((int)c)){ + entries[i]->key[p] = c; + p++; + } + } + entries[i]->key[p] = '\0'; + } + + qsort(entries, msa->numseq, sizeof(struct cmp_sort_entry*), cmp_sort_by_key); + + for(i = 0; i < msa->numseq; i++){ + msa->sequences[i] = entries[i]->seq; + } + + for(i = 0; i < msa->numseq; i++){ + MFREE(entries[i]->key); + MFREE(entries[i]); + } + MFREE(entries); + return OK; +ERROR: + if(entries){ + for(i = 0; i < msa->numseq; i++){ + if(entries[i]){ + MFREE(entries[i]->key); + MFREE(entries[i]); + } + } + MFREE(entries); + } + return FAIL; +} + static int compare_pair(char *seq1A, char *seq2A, char *seq1B, char *seq2B, int len_a, int len_b, struct cmp_stats *stat); @@ -57,11 +147,8 @@ int kalign_msa_compare(struct msa *r, struct msa *t, float *score) t->alnlen = t->sequences[0]->len; } - RUN(kalign_check_msa(r,1)); - RUN(kalign_check_msa(t,1)); - - kalign_sort_msa(r); - kalign_sort_msa(t); + RUN(sort_msa_for_comparison(r)); + RUN(sort_msa_for_comparison(t)); MMALLOC(stat, sizeof(struct cmp_stats)); stat->identical_gaps = 0; @@ -439,11 +526,8 @@ int kalign_msa_compare_detailed(struct msa *r, struct msa *t, t->alnlen = t->sequences[0]->len; } - RUN(kalign_check_msa(r, 1)); - RUN(kalign_check_msa(t, 1)); - - kalign_sort_msa(r); - kalign_sort_msa(t); + RUN(sort_msa_for_comparison(r)); + RUN(sort_msa_for_comparison(t)); ASSERT(r->alnlen > 0, "Reference alignment has length 0"); @@ -496,11 +580,8 @@ int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, t->alnlen = t->sequences[0]->len; } - RUN(kalign_check_msa(r, 1)); - RUN(kalign_check_msa(t, 1)); - - kalign_sort_msa(r); - kalign_sort_msa(t); + RUN(sort_msa_for_comparison(r)); + RUN(sort_msa_for_comparison(t)); ASSERT(n_cols == r->alnlen, "Mask length (%d) != reference alignment length (%d)", diff --git a/lib/src/msa_op.c b/lib/src/msa_op.c index b1b8620..6f5fd2a 100644 --- a/lib/src/msa_op.c +++ b/lib/src/msa_op.c @@ -308,7 +308,7 @@ int set_sip_nsip(struct msa* msa) for (i =0;i < msa->num_profiles;i++){ msa->sip[i] = NULL; msa->nsip[i] = 0; - + msa->plen[i] = 0; } for(i = 0;i < msa->numseq;i++){ @@ -480,6 +480,7 @@ int kalign_arr_to_msa(char** input_sequences, int* len, int numseq,struct msa** seq->alloc_len = len[i]+1; MMALLOC(seq->name, sizeof(char)* MSA_NAME_LEN); + snprintf(seq->name, MSA_NAME_LEN, "seq%d", i); MMALLOC(seq->seq, sizeof(char) * seq->alloc_len); MMALLOC(seq->s, sizeof(uint8_t) * seq->alloc_len); diff --git a/lib/src/task.c b/lib/src/task.c index ce12fe5..5596193 100644 --- a/lib/src/task.c +++ b/lib/src/task.c @@ -183,7 +183,13 @@ int alloc_tasks(struct aln_tasks** tasks,int numseq) for(i = 0; i < t->n_alloc_tasks;i++){ t->list[i] = NULL; MMALLOC(t->list[i], sizeof(struct task)); + t->list[i]->score = 0.0F; t->list[i]->confidence = 0.0F; + t->list[i]->a = 0; + t->list[i]->b = 0; + t->list[i]->c = 0; + t->list[i]->p = 0; + t->list[i]->n = 0; } *tasks = t; diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index 8c588dc..af3f694 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -67,6 +67,7 @@ def __repr__(self): PROTEIN_PFASUM60 = _core.PROTEIN_PFASUM60 PROTEIN_PFASUM_AUTO = _core.PROTEIN_PFASUM_AUTO PROTEIN_DIVERGENT = _core.PROTEIN_DIVERGENT +PROTEIN_CORBLOSUM66 = _core.PROTEIN_CORBLOSUM66 AUTO = _core.AUTO # Refinement mode constants @@ -78,18 +79,49 @@ def __repr__(self): # Mode constants MODE_DEFAULT = "default" MODE_FAST = "fast" -MODE_PRECISE = "precise" - -# Mode preset definitions +MODE_ACCURATE = "accurate" +MODE_PRECISE = "precise" # deprecated alias for "accurate" + +# Named modes with NSGA-III optimized protein presets. +# Each mode is a Pareto-optimal configuration trading accuracy vs speed, +# derived from multi-objective optimization on BAliBASE v4 (218 families). +# +# When mode is one of these AND no explicit gap/matrix overrides are provided, +# the C-side kalign_get_mode_preset() function is used directly, which sets +# per-run heterogeneous gap penalties and scoring matrices. +_PRESET_MODES = {"fast", "default", "accurate"} + +# Legacy mode presets (used when explicit param overrides are present). +# Values match C-side kalign_get_mode_preset() v2 from NSGA-III optimization. _MODE_PRESETS = { - "default": {"vsm_amax": -1.0, "consistency": 5, "consistency_weight": 2.0}, - "fast": {"vsm_amax": -1.0, "consistency": 0, "consistency_weight": 2.0}, - "precise": { - "vsm_amax": -1.0, - "ensemble": 3, - "realign": 1, + "fast": { + "vsm_amax": 1.448, + "seq_weights": 1.063, + "consistency": 0, + "consistency_weight": 1.724, + }, + "default": { + "vsm_amax": 1.885, + "seq_weights": 0.592, + "ensemble": 5, + "consistency": 0, + "consistency_weight": 0.638, + }, + "accurate": { + "vsm_amax": 1.682, + "seq_weights": 1.48, + "ensemble": 5, + "realign": 2, + "consistency": 0, + "consistency_weight": 1.317, + }, + "precise": { # deprecated alias + "vsm_amax": 1.682, + "seq_weights": 1.48, + "ensemble": 5, + "realign": 2, "consistency": 0, - "consistency_weight": 2.0, + "consistency_weight": 1.317, }, } @@ -514,10 +546,22 @@ def _infer_skbio_type(sequences, skbio_seq): def _resolve_mode(mode, explicit_kwargs): """Resolve mode presets, letting explicit parameters override. + Mode presets were derived from NSGA-III multi-objective optimization + (objectives: BAliBASE F1, TC, wall-clock time) with 5-fold cross- + validation on BAliBASE v4 (218 protein families). Each tier represents + a Pareto-optimal configuration: + + - "fast": Single run, optimized gap penalties. ~F1 0.74. + - "default": 3-run ensemble with heterogeneous scoring. ~F1 0.79. + - "accurate": 5-run ensemble with confident refinement. ~F1 0.81. + + Explicit keyword arguments override the preset values. + Parameters ---------- mode : str or None - One of "default", "fast", "precise", or None (treated as "default"). + One of "default", "fast", "accurate", or None (treated as "default"). + "precise" is accepted as a deprecated alias for "accurate". explicit_kwargs : dict Only keys that the caller *explicitly* passed (not sentinel/default). @@ -526,12 +570,23 @@ def _resolve_mode(mode, explicit_kwargs): dict Merged parameter values: mode defaults + explicit overrides. """ + import warnings + if mode is None: mode = "default" mode_lower = mode.lower() + + if mode_lower == "precise": + warnings.warn( + 'mode="precise" is deprecated, use mode="accurate" instead.', + DeprecationWarning, + stacklevel=3, + ) + mode_lower = "accurate" + if mode_lower not in _MODE_PRESETS: raise ValueError( - f"Invalid mode: {mode!r}. Must be one of: 'default', 'fast', 'precise'" + f"Invalid mode: {mode!r}. Must be one of: 'default', 'fast', 'accurate'" ) result = dict(_MODE_PRESETS[mode_lower]) result.update(explicit_kwargs) @@ -1067,16 +1122,48 @@ def align_file_to_file( else: seq_type_int = seq_type + if n_threads is None: + n_threads = get_num_threads() + + # Handle "precise" → "accurate" alias + import warnings as _w + effective_mode = mode + if mode is not None and mode.lower() == "precise": + _w.warn( + 'mode="precise" is deprecated, use mode="accurate" instead.', + DeprecationWarning, + stacklevel=2, + ) + effective_mode = "accurate" + + # Fast path: if a known preset mode is used with no parameter overrides, + # delegate entirely to the C-side NSGA-III optimized presets. + # Check BEFORE converting None → -1.0 sentinels. + _has_overrides = ( + gap_open is not None or gap_extend is not None + or terminal_gap_extend is not None + or ensemble != 0 or realign != 0 + or vsm_amax != -1.0 or consistency != 5 + or consistency_weight != 2.0 or refine != "none" + or seq_weights != 0.0 + ) + if (effective_mode is not None + and effective_mode.lower() in _PRESET_MODES + and not _has_overrides): + _core.align_file_to_file_mode( + input_file, output_file, + effective_mode.lower(), format, n_threads, + ) + return + if gap_open is None: gap_open = -1.0 if gap_extend is None: gap_extend = -1.0 if terminal_gap_extend is None: terminal_gap_extend = -1.0 - if n_threads is None: - n_threads = get_num_threads() - # Resolve mode presets + # Legacy path: resolve mode presets and pass individual params _explicit = {} if ensemble != 0: _explicit["ensemble"] = ensemble @@ -1089,23 +1176,23 @@ def align_file_to_file( if vsm_amax != -1.0: _explicit["vsm_amax"] = vsm_amax - resolved = _resolve_mode(mode, _explicit) + resolved = _resolve_mode(effective_mode, _explicit) ensemble = resolved.get("ensemble", ensemble) realign = resolved.get("realign", realign) consistency = resolved.get("consistency", consistency) consistency_weight = resolved.get("consistency_weight", consistency_weight) vsm_amax = resolved.get("vsm_amax", vsm_amax) - refine_int = _parse_refine_mode(refine) + refine_int = _parse_refine_mode(resolved.get("refine", refine)) _core.align_file_to_file( input_file, output_file, format, seq_type_int, - gap_open, - gap_extend, - terminal_gap_extend, + gap_open if gap_open is not None else -1.0, + gap_extend if gap_extend is not None else -1.0, + terminal_gap_extend if terminal_gap_extend is not None else -1.0, n_threads, refine_int, int(adaptive_budget), @@ -1117,7 +1204,7 @@ def align_file_to_file( realign, save_poar, load_poar, - float(seq_weights), + float(resolved.get("seq_weights", seq_weights)), consistency, consistency_weight, ) @@ -1147,6 +1234,7 @@ def align_file_to_file( "PROTEIN_PFASUM60", "PROTEIN_PFASUM_AUTO", "PROTEIN_DIVERGENT", + "PROTEIN_CORBLOSUM66", "AUTO", "REFINE_NONE", "REFINE_ALL", @@ -1154,6 +1242,7 @@ def align_file_to_file( "REFINE_INLINE", "MODE_DEFAULT", "MODE_FAST", + "MODE_ACCURATE", "MODE_PRECISE", "__version__", "__author__", diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index 207ae58..79491c6 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -594,6 +594,47 @@ void ensemble_custom_file_to_file( } } +// Align using a named mode preset (fast/default/accurate). +// The C library provides NSGA-III optimized protein presets. +void align_file_to_file_mode( + const std::string& input_file, + const std::string& output_file, + const std::string& mode, + const std::string& format = "fasta", + int n_threads = 1 +) { + struct msa* msa_data = nullptr; + int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); + if (result != 0 || !msa_data) { + throw std::runtime_error("Failed to read input file: " + input_file); + } + + struct kalign_run_config runs[KALIGN_MAX_PRESET_RUNS]; + struct kalign_ensemble_config ens; + int n_runs = 0; + + result = kalign_get_mode_preset(mode.c_str(), runs, &n_runs, &ens); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::invalid_argument("Unknown mode: " + mode); + } + + result = kalign_align_full(msa_data, runs, n_runs, + n_runs > 1 ? &ens : nullptr, n_threads); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Alignment failed with error code: " + std::to_string(result)); + } + + result = kalign_write_msa(msa_data, const_cast(output_file.c_str()), + const_cast(format.c_str())); + kalign_free_msa(msa_data); + + if (result != 0) { + throw std::runtime_error("Failed to write output file: " + output_file); + } +} + PYBIND11_MODULE(_core, m) { m.doc() = "Python bindings for Kalign multiple sequence alignment"; @@ -865,6 +906,33 @@ PYBIND11_MODULE(_core, m) { Empty = use seq_type for all runs. )pbdoc"); + // Mode-based alignment (NSGA-III optimized presets) + m.def("align_file_to_file_mode", &align_file_to_file_mode, + py::arg("input_file"), + py::arg("output_file"), + py::arg("mode"), + py::arg("format") = "fasta", + py::arg("n_threads") = 1, + R"pbdoc( + Align sequences using a named mode preset. + + Uses NSGA-III optimized protein presets with per-run + heterogeneous gap penalties and scoring matrices. + + Parameters + ---------- + input_file : str + Path to input sequence file + output_file : str + Path to output alignment file + mode : str + One of "fast", "default", "accurate" + format : str, optional + Output format (default: "fasta") + n_threads : int, optional + Number of threads (default: 1) + )pbdoc"); + // Constants for sequence types m.attr("DNA") = KALIGN_TYPE_DNA; m.attr("DNA_INTERNAL") = KALIGN_TYPE_DNA_INTERNAL; @@ -874,6 +942,7 @@ PYBIND11_MODULE(_core, m) { m.attr("PROTEIN_PFASUM60") = KALIGN_TYPE_PROTEIN_PFASUM60; m.attr("PROTEIN_PFASUM_AUTO") = KALIGN_TYPE_PROTEIN_PFASUM_AUTO; m.attr("PROTEIN_DIVERGENT") = KALIGN_TYPE_PROTEIN_DIVERGENT; + m.attr("PROTEIN_CORBLOSUM66") = KALIGN_TYPE_PROTEIN_CORBLOSUM66; m.attr("AUTO") = KALIGN_TYPE_UNDEFINED; // Constants for refinement modes From ae4ce12d8df2dd15c23748178d1bf15e6e145e7f Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Thu, 12 Mar 2026 15:58:34 +0800 Subject: [PATCH 03/29] Clean 2-path architecture with per-run optimizer parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify Python API to mode-based presets (fast/default/accurate) as the only public interface. Remove all legacy backward-compatibility code and deprecated parameters (ensemble, refine, vsm_amax, etc. from align*()). Add per-run support for vsm_amax and refine in the NSGA-III optimizer via optional array parameters in ensemble_custom_file_to_file(). The C binding also accepts per-run seq_weights, realign, consistency_anchors, and consistency_weight. This lets the optimizer discover diverse ensemble configurations where each run uses different scoring and refinement. - kalign_run_config: 14-field per-run struct, single C entry point - Public API: mode + optional gap penalty overrides only - Optimizer: ensemble_custom_file_to_file() with per-run arrays - Search space: 41 dimensions (7 per-run × 5 slots + 6 shared) - Old v1 checkpoints can still be resumed (auto-expanded to per-run) Co-Authored-By: Claude Opus 4.6 --- benchmarks/downstream/utils.py | 33 +- benchmarks/optimize_unified.py | 284 +++--- benchmarks/scoring.py | 61 +- benchmarks/view_pareto.py | 27 +- docs/PRD-benchmark-repo-update.md | 304 ++++++ docs/parameter-cleanup-integration.md | 337 +++++++ lib/include/kalign/kalign.h | 50 +- lib/include/kalign/kalign_config.h | 36 +- lib/src/aln_param.c | 39 +- lib/src/aln_wrap.c | 198 ++-- lib/src/ensemble.c | 20 +- python-kalign/__init__.py | 1021 +++++--------------- python-kalign/_core.cpp | 824 +++++++--------- python-kalign/cli.py | 96 +- src/run_kalign.c | 6 +- tests/python/test_ecosystem_integration.py | 2 + tests/python/test_modes.py | 25 +- 17 files changed, 1686 insertions(+), 1677 deletions(-) create mode 100644 docs/PRD-benchmark-repo-update.md create mode 100644 docs/parameter-cleanup-integration.md diff --git a/benchmarks/downstream/utils.py b/benchmarks/downstream/utils.py index ba27088..b97930b 100644 --- a/benchmarks/downstream/utils.py +++ b/benchmarks/downstream/utils.py @@ -449,24 +449,17 @@ def _parse_fasta_string(text: str) -> tuple[list[str], list[str]]: def run_kalign( input_fasta: Path, - ensemble: int = 0, + mode: str = "default", seq_type: str = "auto", - tgpo: float | None = None, - terminal_dist_scale: float | None = None, - refine: str | None = None, - vsm_amax: float | None = None, - realign: int | None = None, - consistency: int | None = None, - consistency_weight: float | None = None, ) -> AlignResult: - """Run kalign via the Python API. + """Run kalign via the Python API using mode presets. Parameters ---------- input_fasta : Path Path to unaligned FASTA. - ensemble : int - Number of ensemble runs (0 = off). + mode : str + Alignment mode: "fast", "default", "accurate". seq_type : str Sequence type passed to ``kalign.align_from_file``. @@ -477,26 +470,10 @@ def run_kalign( import kalign as _kalign start = time.perf_counter() - extra = {} - if tgpo is not None: - extra["tgpo"] = tgpo - if terminal_dist_scale is not None: - extra["terminal_dist_scale"] = terminal_dist_scale - if refine is not None: - extra["refine"] = refine - if vsm_amax is not None: - extra["vsm_amax"] = vsm_amax - if realign is not None: - extra["realign"] = realign - if consistency is not None: - extra["consistency"] = consistency - if consistency_weight is not None: - extra["consistency_weight"] = consistency_weight result = _kalign.align_from_file( str(input_fasta), seq_type=seq_type, - ensemble=ensemble, - **extra, + mode=mode, ) wall = time.perf_counter() - start diff --git a/benchmarks/optimize_unified.py b/benchmarks/optimize_unified.py index a8bdf67..822a351 100644 --- a/benchmarks/optimize_unified.py +++ b/benchmarks/optimize_unified.py @@ -12,20 +12,20 @@ 3. Minimize wall time (total CV evaluation time in seconds) Per-run decision variables (x max_runs slots): - - gpo: gap open penalty [2.0, 15.0] - - gpe: gap extend penalty [0.5, 5.0] - - tgpe: terminal gap extend [0.1, 3.0] - - noise: tree perturbation sigma [0.0, 0.5] - - matrix: substitution matrix {PFASUM43, PFASUM60, CorBLOSUM66} + - gpo: gap open penalty [2.0, 15.0] + - gpe: gap extend penalty [0.5, 5.0] + - tgpe: terminal gap extend [0.1, 3.0] + - noise: tree perturbation sigma [0.0, 0.5] + - matrix: substitution matrix {PFASUM43, PFASUM60, CorBLOSUM66} + - vsm_amax: variable scoring matrix [0.0, 5.0] + - refine: post-alignment refinement {NONE, ALL, CONFIDENT, INLINE} Shared decision variables: - n_runs: {1, 3, 5} Core mode variable - - vsm_amax: [0.0, 5.0] Variable scoring matrix - seq_weights: [0.0, 5.0] Profile rebalancing - consistency: {0..6} Anchor consistency rounds - consistency_weight: [0.5, 5.0] Consistency bonus weight - realign: {0, 1, 2} Tree-rebuild iterations - - refine: {0, 1, 2, 3} Post-alignment refinement - min_support: {0..max_runs} POAR consensus threshold Usage: @@ -99,11 +99,11 @@ from rich.text import Text # type: ignore[import-untyped] from kalign._core import ( # type: ignore[import-untyped] - PROTEIN, PROTEIN_PFASUM43, PROTEIN_PFASUM60, DNA, RNA, + MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66, + MATRIX_DNA, MATRIX_RNA, REFINE_NONE, REFINE_ALL, REFINE_CONFIDENT, REFINE_INLINE, ensemble_custom_file_to_file, ) -import kalign # type: ignore[import-untyped] from .datasets import (BenchmarkCase, balibase_cases, balibase_download, balibase_is_available, @@ -133,11 +133,11 @@ "per_run_cont_upper": np.array([15.0, 5.0, 3.0, 0.5]), "shared_cont_lower": np.array([0.0, 0.0, 0.5]), "shared_cont_upper": np.array([5.0, 5.0, 5.0]), - "matrix_map_int": [PROTEIN_PFASUM43, PROTEIN_PFASUM60, PROTEIN], - "matrix_map_str": ["pfasum43", "pfasum60", "protein"], - "matrix_names": {PROTEIN_PFASUM43: "P43", PROTEIN_PFASUM60: "P60", PROTEIN: "CB66"}, + "matrix_map_int": [MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66], + "matrix_map_str": ["pfasum43", "pfasum60", "corblosum66"], + "matrix_names": {MATRIX_PFASUM43: "P43", MATRIX_PFASUM60: "P60", MATRIX_CORBLOSUM66: "CB66"}, "n_matrices": 3, - "seq_type_int": PROTEIN, # C constant for ensemble seq_type + "seq_type_int": MATRIX_PFASUM43, # default protein matrix for ensemble seq_type "seq_type_str": "protein", # Python API string "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range }, @@ -150,11 +150,11 @@ "shared_cont_lower": np.array([0.0, 0.0, 0.5]), "shared_cont_upper": np.array([3.0, 3.0, 5.0]), # RNA has only one scoring type - "matrix_map_int": [RNA], + "matrix_map_int": [MATRIX_RNA], "matrix_map_str": ["rna"], - "matrix_names": {RNA: "RNA"}, + "matrix_names": {MATRIX_RNA: "RNA"}, "n_matrices": 1, - "seq_type_int": RNA, + "seq_type_int": MATRIX_RNA, "seq_type_str": "rna", "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range }, @@ -163,11 +163,11 @@ "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), "shared_cont_lower": np.array([0.0, 0.0, 0.5]), "shared_cont_upper": np.array([3.0, 3.0, 5.0]), - "matrix_map_int": [DNA], + "matrix_map_int": [MATRIX_DNA], "matrix_map_str": ["dna"], - "matrix_names": {DNA: "DNA"}, + "matrix_names": {MATRIX_DNA: "DNA"}, "n_matrices": 1, - "seq_type_int": DNA, + "seq_type_int": MATRIX_DNA, "seq_type_str": "dna", "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range }, @@ -185,14 +185,27 @@ def _matrix_map_int(): return _active_profile["matrix_map_int"] def _matrix_map_str(): return _active_profile["matrix_map_str"] def _matrix_names(): return _active_profile["matrix_names"] -# Legacy aliases for backward compat (used in view_pareto.py etc.) +# Legacy aliases (used in view_pareto.py etc.) MATRIX_MAP_INT = PARAM_PROFILES["protein"]["matrix_map_int"] MATRIX_MAP_STR = PARAM_PROFILES["protein"]["matrix_map_str"] MATRIX_NAMES = PARAM_PROFILES["protein"]["matrix_names"] +# Old constant names used in view_pareto.py and checkpoint data — keep for loading old checkpoints +_OLD_MATRIX_COMPAT = {3: MATRIX_PFASUM43, 5: MATRIX_PFASUM43, 6: MATRIX_PFASUM60} + def get_vars(max_runs: int) -> dict: - """Return pymoo mixed-variable space definition.""" + """Return pymoo mixed-variable space definition. + + Per-run variables (one slot per max_runs): + gpo, gpe, tgpe, noise, matrix — gap/tree params + vsm_amax — variable scoring matrix amplitude + refine — post-alignment refinement mode + + Shared variables: + seq_weights, consistency_weight — scalars applied to all runs + n_runs, consistency, realign, min_support — integer/categorical + """ profile = _active_profile lo = profile["per_run_cont_lower"] hi = profile["per_run_cont_upper"] @@ -206,8 +219,10 @@ def get_vars(max_runs: int) -> dict: variables[f"tgpe_{k}"] = Real(bounds=(float(lo[2]), float(hi[2]))) variables[f"noise_{k}"] = Real(bounds=(float(lo[3]), float(hi[3]))) variables[f"matrix_{k}"] = Choice(options=list(range(profile["n_matrices"]))) + # Per-run VSM amplitude and refinement mode + variables[f"vsm_amax_{k}"] = Real(bounds=(float(slo[0]), float(shi[0]))) + variables[f"refine_{k}"] = Choice(options=REFINE_MAP) - variables["vsm_amax"] = Real(bounds=(float(slo[0]), float(shi[0]))) variables["seq_weights"] = Real(bounds=(float(slo[1]), float(shi[1]))) variables["consistency_weight"] = Real(bounds=(float(slo[2]), float(shi[2]))) @@ -215,7 +230,6 @@ def get_vars(max_runs: int) -> dict: variables["n_runs"] = Choice(options=N_RUNS_MAP) variables["consistency"] = Choice(options=consistency_options) variables["realign"] = Integer(bounds=(0, 2)) - variables["refine"] = Choice(options=REFINE_MAP) variables["min_support"] = Integer(bounds=(0, max_runs)) return variables @@ -226,19 +240,28 @@ def decode_unified_params(x, max_runs: int): x is a dict with keys like 'gpo_0', 'n_runs', 'consistency', etc. Values are native types (float for Real, int for Integer/Choice). + + vsm_amax and refine are per-run (vsm_amax_{k}, refine_{k}). + If the per-run keys are missing (old checkpoint), falls back to + shared 'vsm_amax' / 'refine' keys. """ n_runs = int(x["n_runs"]) consistency = int(x["consistency"]) realign = int(x["realign"]) - refine = int(x["refine"]) min_support_raw = int(x["min_support"]) - vsm_amax = float(x["vsm_amax"]) seq_weights = float(x["seq_weights"]) consistency_weight = float(x["consistency_weight"]) run_gpo, run_gpe, run_tgpe, run_noise = [], [], [], [] run_types, run_matrices = [], [] + run_vsm_amax, run_refine = [], [] + + # Detect old checkpoint format (shared vsm_amax / refine) + has_per_run_vsm = f"vsm_amax_0" in x + has_per_run_refine = f"refine_0" in x + shared_vsm = float(x.get("vsm_amax", 0.0)) if not has_per_run_vsm else 0.0 + shared_refine = int(x.get("refine", REFINE_NONE)) if not has_per_run_refine else REFINE_NONE for k in range(n_runs): run_gpo.append(float(x[f"gpo_{k}"])) @@ -248,6 +271,8 @@ def decode_unified_params(x, max_runs: int): matrix_idx = int(x[f"matrix_{k}"]) run_types.append(_matrix_map_int()[matrix_idx]) run_matrices.append(_matrix_map_str()[matrix_idx]) + run_vsm_amax.append(float(x[f"vsm_amax_{k}"]) if has_per_run_vsm else shared_vsm) + run_refine.append(int(x[f"refine_{k}"]) if has_per_run_refine else shared_refine) # --- Masking rules --- @@ -274,12 +299,12 @@ def decode_unified_params(x, max_runs: int): "run_noise": run_noise, "run_types": run_types, "run_matrices": run_matrices, - "vsm_amax": vsm_amax, + "run_vsm_amax": run_vsm_amax, + "run_refine": run_refine, "seq_weights": seq_weights, "consistency_weight": consistency_weight, "consistency": consistency, "realign": realign, - "refine": refine, "min_support": min_support, } @@ -294,20 +319,22 @@ def encode_unified_params(params, max_runs: int) -> dict: x[f"tgpe_{k}"] = params["run_tgpe"][k] x[f"noise_{k}"] = params["run_noise"][k] x[f"matrix_{k}"] = _matrix_map_int().index(params["run_types"][k]) + x[f"vsm_amax_{k}"] = params["run_vsm_amax"][k] + x[f"refine_{k}"] = params["run_refine"][k] else: x[f"gpo_{k}"] = params["run_gpo"][0] x[f"gpe_{k}"] = params["run_gpe"][0] x[f"tgpe_{k}"] = params["run_tgpe"][0] x[f"noise_{k}"] = params["run_noise"][0] x[f"matrix_{k}"] = _matrix_map_int().index(params["run_types"][0]) + x[f"vsm_amax_{k}"] = params["run_vsm_amax"][0] + x[f"refine_{k}"] = params["run_refine"][0] - x["vsm_amax"] = params["vsm_amax"] x["seq_weights"] = params["seq_weights"] x["consistency_weight"] = params["consistency_weight"] x["n_runs"] = params["n_runs"] x["consistency"] = params["consistency"] x["realign"] = params["realign"] - x["refine"] = params["refine"] x["min_support"] = params["min_support"] return x @@ -321,17 +348,20 @@ def mode_label(params): def format_unified_short(params): """Compact one-line summary.""" n_runs = params["n_runs"] - ref = REFINE_NAMES.get(params["refine"], "?") if n_runs == 1: mat = _matrix_names().get(params["run_types"][0], "?") + ref = REFINE_NAMES.get(params["run_refine"][0], "?") return (f"{mode_label(params)} {mat} gpo={params['run_gpo'][0]:.1f} " - f"vsm={params['vsm_amax']:.1f} sw={params['seq_weights']:.1f} " + f"vsm={params['run_vsm_amax'][0]:.1f} sw={params['seq_weights']:.1f} " f"c={params['consistency']} re={params['realign']} ref={ref}") else: - return (f"{mode_label(params)} vsm={params['vsm_amax']:.1f} " + # Show per-run refine modes compactly + refs = "/".join(REFINE_NAMES.get(r, "?") for r in params["run_refine"]) + vsms = "/".join(f"{v:.1f}" for v in params["run_vsm_amax"]) + return (f"{mode_label(params)} vsm={vsms} " f"sw={params['seq_weights']:.1f} c={params['consistency']} " - f"re={params['realign']} ref={ref} ms={params['min_support']}") + f"re={params['realign']} ref={refs} ms={params['min_support']}") def format_unified_long(params): @@ -339,16 +369,16 @@ def format_unified_long(params): lines = [f"mode={mode_label(params)} n_runs={params['n_runs']}"] for k in range(params["n_runs"]): mat = _matrix_names().get(params["run_types"][k], "?") + ref = REFINE_LONG.get(params["run_refine"][k], "?") lines.append(f" run_{k}: gpo={params['run_gpo'][k]:.3f} " f"gpe={params['run_gpe'][k]:.3f} " f"tgpe={params['run_tgpe'][k]:.3f} " - f"noise={params['run_noise'][k]:.3f} {mat}") - ref = REFINE_LONG.get(params["refine"], "?") - lines.append(f" vsm_amax={params['vsm_amax']:.3f} " - f"seq_weights={params['seq_weights']:.3f}") + f"noise={params['run_noise'][k]:.3f} {mat} " + f"vsm={params['run_vsm_amax'][k]:.3f} ref={ref}") + lines.append(f" seq_weights={params['seq_weights']:.3f}") lines.append(f" consistency={params['consistency']} " f"consistency_weight={params['consistency_weight']:.3f}") - lines.append(f" realign={params['realign']} refine={ref} " + lines.append(f" realign={params['realign']} " f"min_support={params['min_support']}") return "\n".join(lines) @@ -390,7 +420,6 @@ def evaluate_unified(params, cases, n_threads=1, quiet=True): """Run kalign with unified params on all cases, return mean metrics.""" results_by_cat: Dict[str, list] = {} total_time = 0.0 - n_runs = params["n_runs"] for case in cases: with tempfile.TemporaryDirectory() as tmpdir: @@ -399,47 +428,30 @@ def evaluate_unified(params, cases, n_threads=1, quiet=True): try: start = time.perf_counter() - if n_runs == 1: - # Single-run path - kalign.align_file_to_file( - str(case.unaligned), - str(output), - format="fasta", - seq_type=params["run_matrices"][0], - gap_open=params["run_gpo"][0], - gap_extend=params["run_gpe"][0], - terminal_gap_extend=params["run_tgpe"][0], - n_threads=n_threads, - vsm_amax=params["vsm_amax"], - seq_weights=params["seq_weights"], - consistency=params["consistency"], - consistency_weight=params["consistency_weight"], - realign=params["realign"], - refine=params["refine"], - mode=None, - ) - else: - # Ensemble path - ensemble_custom_file_to_file( - str(case.unaligned), - str(output), - run_gpo=params["run_gpo"], - run_gpe=params["run_gpe"], - run_tgpe=params["run_tgpe"], - run_noise=params["run_noise"], - run_types=params["run_types"], - format="fasta", - seq_type=_active_profile["seq_type_int"], - seed=42, - min_support=params["min_support"], - refine=params["refine"], - vsm_amax=params["vsm_amax"], - realign=params["realign"], - seq_weights=params["seq_weights"], - n_threads=n_threads, - consistency_anchors=params["consistency"], - consistency_weight=params["consistency_weight"], - ) + # Always use ensemble_custom_file_to_file — it handles + # n_runs=1 just fine and is the only path that accepts + # all fine-grained optimizer parameters. + ensemble_custom_file_to_file( + str(case.unaligned), + str(output), + run_gpo=params["run_gpo"], + run_gpe=params["run_gpe"], + run_tgpe=params["run_tgpe"], + run_noise=params["run_noise"], + run_types=params["run_types"], + format="fasta", + seq_type=_active_profile["seq_type_int"], + seed=42, + min_support=params["min_support"], + realign=params["realign"], + seq_weights=params["seq_weights"], + n_threads=n_threads, + consistency_anchors=params["consistency"], + consistency_weight=params["consistency_weight"], + # Per-run overrides + run_vsm_amax=params["run_vsm_amax"], + run_refine=params["run_refine"], + ) wall_time = time.perf_counter() - start total_time += wall_time @@ -597,19 +609,20 @@ def _build_status_panel(self): def _format_best_entry(self, label, e, delta_str): """Format a best-of entry with full parameter details.""" p = e["params"] - ref = REFINE_NAMES.get(p.get("refine", 0), "?") header = f"{label} {mode_label(p)} {e['wall_time']:.0f}s {delta_str}" - # Per-run gap penalties + # Per-run details run_parts = [] for k in range(p["n_runs"]): mat = MATRIX_NAMES.get(p["run_types"][k], "?") + ref = REFINE_NAMES.get(p["run_refine"][k], "?") noise_str = f" n={p['run_noise'][k]:.2f}" if p["run_noise"][k] > 0 else "" run_parts.append(f" R{k}: gpo={p['run_gpo'][k]:.2f} " f"gpe={p['run_gpe'][k]:.2f} " - f"tgpe={p['run_tgpe'][k]:.2f}{noise_str} {mat}") - shared = (f" vsm={p['vsm_amax']:.2f} sw={p['seq_weights']:.2f} " + f"tgpe={p['run_tgpe'][k]:.2f}{noise_str} {mat} " + f"vsm={p['run_vsm_amax'][k]:.2f} ref={ref}") + shared = (f" sw={p['seq_weights']:.2f} " f"c={p['consistency']} cw={p['consistency_weight']:.2f} " - f"re={p['realign']} ref={ref} ms={p['min_support']}") + f"re={p['realign']} ms={p['min_support']}") return "\n".join([header] + run_parts + [shared]) def _build_best_panel(self): @@ -654,17 +667,17 @@ def _build_pareto_table(self): p = entry.get("params", {}) f1_style = "bold green" if entry["f1"] > bl_f1 else "" tc_style = "bold green" if entry["tc"] > bl_tc else "" - ref = REFINE_NAMES.get(p.get("refine", 0), "?") + refs = "/".join(REFINE_NAMES.get(r, "?") for r in p.get("run_refine", [0])) # Build detailed params string run_strs = [] for k in range(p.get("n_runs", 1)): mat = MATRIX_NAMES.get(p["run_types"][k], "?") + vsm = p["run_vsm_amax"][k] run_strs.append(f"R{k}:{p['run_gpo'][k]:.1f}/{p['run_gpe'][k]:.2f}/" - f"{p['run_tgpe'][k]:.2f}/{mat}") + f"{p['run_tgpe'][k]:.2f}/{mat}/v{vsm:.1f}") runs = " ".join(run_strs) - shared = (f"vsm={p.get('vsm_amax', 0):.1f} " - f"sw={p.get('seq_weights', 0):.1f} " + shared = (f"sw={p.get('seq_weights', 0):.1f} " f"ms={p.get('min_support', 0)}") params_str = f"{runs} | {shared}" @@ -676,7 +689,7 @@ def _build_pareto_table(self): f"{entry.get('wall_time', 0):.0f}s", str(p.get("consistency", 0)), str(p.get("realign", 0)), - ref, + refs, params_str, ) return table @@ -701,14 +714,15 @@ def _build_recent_panel(self): lines = [] for e in self.recent_evals[-5:]: p = e.get("params", {}) - ref = REFINE_NAMES.get(p.get("refine", 0), "?") + ref0 = REFINE_NAMES.get(p["run_refine"][0], "?") if p.get("run_refine") else "?" mat = MATRIX_NAMES.get(p["run_types"][0], "?") if p.get("run_types") else "?" gpo = p["run_gpo"][0] if p.get("run_gpo") else 0 + vsm0 = p["run_vsm_amax"][0] if p.get("run_vsm_amax") else 0 lines.append(f"F1={e['f1']:.4f} TC={e['tc']:.4f} " f"t={e['wall_time']:.0f}s {mode_label(p)} " f"gpo={gpo:.1f} {mat} " - f"vsm={p.get('vsm_amax', 0):.1f} " - f"c={p.get('consistency', 0)} re={p.get('realign', 0)} ref={ref}") + f"vsm={vsm0:.1f} " + f"c={p.get('consistency', 0)} re={p.get('realign', 0)} ref={ref0}") return Panel("\n".join(lines) if lines else "(none)", title="Recent") def _build_layout(self): @@ -978,7 +992,7 @@ def notify(self, algorithm): raw_X = pop.get("X") pop_X = np.array([dict(d) for d in raw_X], dtype=object) ckpt = { - "format": "mixed_v1", + "format": "mixed_v2", "pop_X": pop_X, "pop_F": pop.get("F").copy(), "pop_G": pop.get("G"), @@ -1010,25 +1024,29 @@ def load_checkpoint(path: Path): "fast": { "n_runs": 1, "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [PROTEIN_PFASUM60], "run_matrices": ["pfasum60"], - "vsm_amax": 2.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 0, "refine": REFINE_NONE, "min_support": 0, + "run_types": [MATRIX_PFASUM60], "run_matrices": ["pfasum60"], + "run_vsm_amax": [2.0], "run_refine": [REFINE_NONE], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 0, "min_support": 0, }, "accurate": { "n_runs": 1, "run_gpo": [8.472], "run_gpe": [0.554], "run_tgpe": [0.409], "run_noise": [0.0], - "run_types": [PROTEIN_PFASUM60], "run_matrices": ["pfasum60"], - "vsm_amax": 1.359, "seq_weights": 3.407, "consistency_weight": 1.167, - "consistency": 8, "realign": 2, "refine": REFINE_NONE, "min_support": 0, + "run_types": [MATRIX_PFASUM60], "run_matrices": ["pfasum60"], + "run_vsm_amax": [1.359], "run_refine": [REFINE_NONE], + "seq_weights": 3.407, "consistency_weight": 1.167, + "consistency": 8, "realign": 2, "min_support": 0, }, "ensemble": { "n_runs": 3, "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], - "run_types": [PROTEIN_PFASUM60, PROTEIN_PFASUM60, PROTEIN_PFASUM60], + "run_types": [MATRIX_PFASUM60, MATRIX_PFASUM60, MATRIX_PFASUM60], "run_matrices": ["pfasum60", "pfasum60", "pfasum60"], - "vsm_amax": 2.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 1, "refine": REFINE_CONFIDENT, "min_support": 0, + "run_vsm_amax": [2.0, 2.0, 2.0], + "run_refine": [REFINE_CONFIDENT, REFINE_CONFIDENT, REFINE_CONFIDENT], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 1, "min_support": 0, }, } @@ -1036,24 +1054,28 @@ def load_checkpoint(path: Path): "fast": { "n_runs": 1, "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [RNA], "run_matrices": ["rna"], - "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 0, "refine": REFINE_NONE, "min_support": 0, + "run_types": [MATRIX_RNA], "run_matrices": ["rna"], + "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 0, "min_support": 0, }, "accurate": { "n_runs": 1, "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [RNA], "run_matrices": ["rna"], - "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 8, "realign": 2, "refine": REFINE_NONE, "min_support": 0, + "run_types": [MATRIX_RNA], "run_matrices": ["rna"], + "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 8, "realign": 2, "min_support": 0, }, "ensemble": { "n_runs": 3, "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], - "run_types": [RNA, RNA, RNA], "run_matrices": ["rna", "rna", "rna"], - "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 1, "refine": REFINE_CONFIDENT, "min_support": 0, + "run_types": [MATRIX_RNA, MATRIX_RNA, MATRIX_RNA], "run_matrices": ["rna", "rna", "rna"], + "run_vsm_amax": [0.0, 0.0, 0.0], + "run_refine": [REFINE_CONFIDENT, REFINE_CONFIDENT, REFINE_CONFIDENT], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 1, "min_support": 0, }, } @@ -1061,24 +1083,28 @@ def load_checkpoint(path: Path): "fast": { "n_runs": 1, "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [DNA], "run_matrices": ["dna"], - "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 0, "refine": REFINE_NONE, "min_support": 0, + "run_types": [MATRIX_DNA], "run_matrices": ["dna"], + "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 0, "realign": 0, "min_support": 0, }, "accurate": { "n_runs": 1, "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [DNA], "run_matrices": ["dna"], - "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 8, "realign": 2, "refine": REFINE_NONE, "min_support": 0, + "run_types": [MATRIX_DNA], "run_matrices": ["dna"], + "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 8, "realign": 2, "min_support": 0, }, "ensemble": { "n_runs": 3, "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], - "run_types": [DNA, DNA, DNA], "run_matrices": ["dna", "dna", "dna"], - "vsm_amax": 0.0, "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 8, "realign": 1, "refine": REFINE_CONFIDENT, "min_support": 0, + "run_types": [MATRIX_DNA, MATRIX_DNA, MATRIX_DNA], "run_matrices": ["dna", "dna", "dna"], + "run_vsm_amax": [0.0, 0.0, 0.0], + "run_refine": [REFINE_CONFIDENT, REFINE_CONFIDENT, REFINE_CONFIDENT], + "seq_weights": 0.0, "consistency_weight": 2.0, + "consistency": 8, "realign": 1, "min_support": 0, }, } @@ -1287,11 +1313,15 @@ def main(): console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") return ckpt = load_checkpoint(resume_path) - if ckpt.get("format") != "mixed_v1": + ckpt_fmt = ckpt.get("format") + if ckpt_fmt not in ("mixed_v1", "mixed_v2"): console.print("[bold red]Cannot resume from old-format checkpoint.[/]") console.print("Old checkpoints used float arrays; new format uses mixed variables.") console.print("Please start a fresh optimization run (remove --resume).") return + if ckpt_fmt == "mixed_v1": + console.print("[bold yellow]Note:[/] resuming from v1 checkpoint. " + "Old shared vsm_amax/refine will be expanded to per-run arrays.") if ckpt.get("max_runs") != max_runs: console.print(f"[bold red]max_runs mismatch:[/] checkpoint has " f"{ckpt.get('max_runs')}, requested {max_runs}") @@ -1495,16 +1525,16 @@ def main(): f.write(f" n_runs={p['n_runs']}\n") for run_k in range(p["n_runs"]): mat = MATRIX_NAMES.get(p["run_types"][run_k], "?") + ref = REFINE_LONG.get(p["run_refine"][run_k], "?") f.write(f" run_{run_k}: gpo={p['run_gpo'][run_k]:.3f} " f"gpe={p['run_gpe'][run_k]:.3f} " f"tgpe={p['run_tgpe'][run_k]:.3f} " - f"noise={p['run_noise'][run_k]:.3f} {mat}\n") - ref = REFINE_LONG.get(p["refine"], "?") - f.write(f" vsm_amax={p['vsm_amax']:.3f} " - f"seq_weights={p['seq_weights']:.3f}\n") + f"noise={p['run_noise'][run_k]:.3f} {mat} " + f"vsm={p['run_vsm_amax'][run_k]:.3f} ref={ref}\n") + f.write(f" seq_weights={p['seq_weights']:.3f}\n") f.write(f" consistency={p['consistency']} " f"consistency_weight={p['consistency_weight']:.3f}\n") - f.write(f" realign={p['realign']} refine={ref} " + f.write(f" realign={p['realign']} " f"min_support={p['min_support']}\n\n") console.print(f"Pareto front saved to {summary_path}") diff --git a/benchmarks/scoring.py b/benchmarks/scoring.py index b4b1dab..dad78b1 100644 --- a/benchmarks/scoring.py +++ b/benchmarks/scoring.py @@ -36,31 +36,19 @@ def to_dict(self) -> dict: def align_with_python_api( - case: BenchmarkCase, output: Path, n_threads: int = 1, refine: str = "none", - adaptive_budget: bool = False, ensemble: int = 0, - mode: Optional[str] = None, + case: BenchmarkCase, output: Path, n_threads: int = 1, + mode: str = "default", ) -> float: """Align using kalign Python API. Returns wall time in seconds.""" start = time.perf_counter() - if mode is not None: - kalign.align_file_to_file( - str(case.unaligned), - str(output), - format="fasta", - n_threads=n_threads, - mode=mode, - ) - else: - kalign.align_file_to_file( - str(case.unaligned), - str(output), - format="fasta", - seq_type=case.seq_type, - n_threads=n_threads, - refine=refine, - adaptive_budget=adaptive_budget, - ensemble=ensemble, - ) + kalign.align_file_to_file( + str(case.unaligned), + str(output), + format="fasta", + seq_type=case.seq_type, + n_threads=n_threads, + mode=mode, + ) return time.perf_counter() - start @@ -69,20 +57,13 @@ def align_with_cli( output: Path, binary: str = "kalign", n_threads: int = 1, - refine: str = "none", - adaptive_budget: bool = False, - ensemble: int = 0, + mode: str = "default", ) -> float: """Align using kalign C binary via subprocess. Returns wall time in seconds.""" cmd = [binary, "-i", str(case.unaligned), "-f", "fasta", "-o", str(output)] if n_threads > 1: cmd.extend(["--nthreads", str(n_threads)]) - if refine != "none": - cmd.extend(["--refine", refine]) - if adaptive_budget: - cmd.append("--adaptive-budget") - if ensemble > 0: - cmd.extend(["--ensemble", str(ensemble)]) + cmd.extend(["--mode", mode]) start = time.perf_counter() result = subprocess.run(cmd, capture_output=True, text=True) @@ -200,10 +181,7 @@ def run_case( method: str = "python_api", binary: str = "kalign", n_threads: int = 1, - refine: str = "none", - adaptive_budget: bool = False, - ensemble: int = 0, - mode: Optional[str] = None, + mode: str = "default", ) -> AlignmentResult: """Run alignment + scoring for a single benchmark case.""" with tempfile.TemporaryDirectory() as tmpdir: @@ -212,11 +190,10 @@ def run_case( try: if method == "python_api": wall_time = align_with_python_api( - case, output, n_threads, refine, adaptive_budget, - ensemble, mode=mode, + case, output, n_threads, mode=mode, ) elif method == "cli": - wall_time = align_with_cli(case, output, binary, n_threads, refine, adaptive_budget, ensemble) + wall_time = align_with_cli(case, output, binary, n_threads, mode=mode) elif method in EXTERNAL_TOOLS: wall_time = align_with_external(case, output, method, n_threads) else: @@ -233,8 +210,8 @@ def run_case( sp_score=sp_score, wall_time=wall_time, seq_type=case.seq_type, - refine="n/a" if is_external else (mode or refine), - ensemble=0 if is_external else ensemble, + refine="n/a" if is_external else mode, + ensemble=0, recall=detailed["recall"], precision=detailed["precision"], f1=detailed["f1"], @@ -249,7 +226,7 @@ def run_case( sp_score=0.0, wall_time=0.0, seq_type=case.seq_type, - refine="n/a" if is_external else (mode or refine), - ensemble=0 if is_external else ensemble, + refine="n/a" if is_external else mode, + ensemble=0, error=str(e), ) diff --git a/benchmarks/view_pareto.py b/benchmarks/view_pareto.py index 5386f14..0b2b606 100644 --- a/benchmarks/view_pareto.py +++ b/benchmarks/view_pareto.py @@ -81,21 +81,25 @@ def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: tc = -pop_F[i, 1] wt = pop_F[i, 2] - ref_name = REFINE_LONG.get(params["refine"], "?") mode = mode_label(params) # Per-run details run_details = [] for k in range(params["n_runs"]): mat = MATRIX_NAMES.get(params["run_types"][k], "?") + ref = REFINE_LONG.get(params["run_refine"][k], "?") noise = params["run_noise"][k] noise_str = f" n={noise:.2f}" if noise > 0 else "" run_details.append( f"R{k}: gpo={params['run_gpo'][k]:.2f} " f"gpe={params['run_gpe'][k]:.2f} " - f"tgpe={params['run_tgpe'][k]:.2f}{noise_str} {mat}" + f"tgpe={params['run_tgpe'][k]:.2f}{noise_str} {mat} " + f"vsm={params['run_vsm_amax'][k]:.2f} ref={ref}" ) + # Summarize per-run refine/vsm for the table + refs = "/".join(REFINE_LONG.get(r, "?") for r in params["run_refine"]) + rows.append({ "idx": i, "f1": round(f1, 4), @@ -103,12 +107,12 @@ def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: "wall_time": round(wt, 1), "mode": mode, "n_runs": params["n_runs"], - "vsm_amax": round(params["vsm_amax"], 3), + "vsm_amax_0": round(params["run_vsm_amax"][0], 3), "seq_weights": round(params["seq_weights"], 3), "consistency": params["consistency"], "consistency_weight": round(params["consistency_weight"], 3), "realign": params["realign"], - "refine": ref_name, + "refine": refs, "min_support": params["min_support"], "run_details": "\n".join(run_details), # Flattened run 0 params for color/hover @@ -307,7 +311,7 @@ def _format_tier_config(row) -> str: lines.append(f"# {line}") lines.append(f"config = {{") lines.append(f' "n_runs": {row["n_runs"]},') - lines.append(f' "vsm_amax": {row["vsm_amax"]},') + lines.append(f' "vsm_amax_0": {row["vsm_amax_0"]},') lines.append(f' "seq_weights": {row["seq_weights"]},') lines.append(f' "consistency": {row["consistency"]},') lines.append(f' "consistency_weight": {row["consistency_weight"]},') @@ -340,7 +344,7 @@ def create_app(ckpt_path: str, remote_path: str = "", refresh_sec: int = 30, id="color-by", options=[ {"label": "Mode", "value": "mode"}, - {"label": "VSM amax", "value": "vsm_amax"}, + {"label": "VSM amax", "value": "vsm_amax_0"}, {"label": "Seq weights", "value": "seq_weights"}, {"label": "Consistency", "value": "consistency"}, {"label": "Realign", "value": "realign"}, @@ -465,7 +469,7 @@ def create_app(ckpt_path: str, remote_path: str = "", refresh_sec: int = 30, {"name": "F1", "id": "f1"}, {"name": "TC", "id": "tc"}, {"name": "Time", "id": "wall_time"}, - {"name": "VSM", "id": "vsm_amax"}, + {"name": "VSM", "id": "vsm_amax_0"}, {"name": "SW", "id": "seq_weights"}, {"name": "C", "id": "consistency"}, {"name": "CW", "id": "consistency_weight"}, @@ -559,7 +563,7 @@ def update_all(n_intervals, color_by, x_axis, y_axis, mode_filter, f"Last update: {ago:.0f}s ago") # 2D scatter - hover_data = ["mode", "vsm_amax", "seq_weights", "consistency", + hover_data = ["mode", "vsm_amax_0", "seq_weights", "consistency", "realign", "refine", "gpo_0", "gpe_0", "tgpe_0", "matrix_0", "min_support"] fig2d = px.scatter( @@ -850,6 +854,8 @@ def save_tiers(n_clicks, tier_fast, tier_default, tier_accurate, ckpt_path): "gpe": round(float(full_params["run_gpe"][k]), 4), "tgpe": round(float(full_params["run_tgpe"][k]), 4), "matrix": MATRIX_NAMES.get(full_params["run_types"][k], "?"), + "vsm_amax": round(float(full_params["run_vsm_amax"][k]), 4), + "refine": REFINE_LONG.get(full_params["run_refine"][k], "NONE"), } if full_params["run_noise"][k] > 0: run["noise"] = round(float(full_params["run_noise"][k]), 4) @@ -863,12 +869,10 @@ def save_tiers(n_clicks, tier_fast, tier_default, tier_accurate, ckpt_path): }, "params": { "n_runs": int(row["n_runs"]), - "vsm_amax": float(row["vsm_amax"]), "seq_weights": float(row["seq_weights"]), "consistency": int(row["consistency"]), "consistency_weight": float(row["consistency_weight"]), "realign": int(row["realign"]), - "refine": str(row["refine"]), "min_support": int(row["min_support"]), }, "runs": runs, @@ -948,13 +952,12 @@ def _format_detail(row): row["run_details"], f"", f"Shared parameters:", - f" vsm_amax = {row['vsm_amax']}", f" seq_weights = {row['seq_weights']}", f" consistency = {row['consistency']}", f" consistency_wt = {row['consistency_weight']}", f" realign = {row['realign']}", - f" refine = {row['refine']}", f" min_support = {row['min_support']}", + f" (vsm_amax, refine are per-run — see run details above)", ] return "\n".join(lines) diff --git a/docs/PRD-benchmark-repo-update.md b/docs/PRD-benchmark-repo-update.md new file mode 100644 index 0000000..d315ab6 --- /dev/null +++ b/docs/PRD-benchmark-repo-update.md @@ -0,0 +1,304 @@ +# PRD: Benchmark Repo Update for Kalign v3.5 + +## Context + +The kalign Python bindings have been simplified to a clean two-path +architecture. The old parameter-based API (`ensemble=`, `refine=`, +`vsm_amax=`, `consistency=`, etc.) has been removed from the public +`kalign.align*()` functions. These functions now only accept `mode` +as the configuration mechanism. + +Full technical details: `docs/parameter-cleanup-integration.md` in the +kalign repo. + +**Important**: The benchmark repo's `CLAUDE.md` says "Do not edit +external code". All changes here are ONLY to the benchmark repo. The +kalign library itself is already updated and installed. + +--- + +## Prerequisites + +Install the updated kalign from source: + +```bash +cd /Users/timo/code/kalign +uv pip install -e . +``` + +Verify: +```bash +uv run python -c "import kalign; print(kalign.__version__); print(kalign.align(['ATCG','ATCGG']))" +``` + +--- + +## Kalign API Quick Reference + +### Standard alignment (mode-based) + +```python +import kalign + +# Three modes: "fast", "default", "accurate" +result = kalign.align_from_file("input.fasta", mode="default") +names, sequences = result # AlignedSequences unpacks as 2-tuple + +# File-to-file +kalign.align_file_to_file("input.fasta", "output.fasta", mode="default") +``` + +Parameters accepted by all three `align*()` functions: +- `mode`: `"fast"` | `"default"` | `"accurate"` (default: `"default"`) +- `seq_type`: `"auto"` | `"dna"` | `"rna"` | `"protein"` (default: `"auto"`) +- `gap_open`, `gap_extend`, `terminal_gap_extend`: optional float overrides + (when any is set, mode is forced to `"fast"`) +- `n_threads`: int (default: 1) + +No other parameters exist. There is no `ensemble`, `refine`, `vsm_amax`, +`realign`, `consistency`, `seq_weights`, etc. in the public API. These +are handled internally by the mode presets. + +### Optimizer path (for NSGA-III evaluation) + +```python +from kalign._core import ensemble_custom_file_to_file + +ensemble_custom_file_to_file( + input_file, output_file, + run_gpo=[...], run_gpe=[...], run_tgpe=[...], run_noise=[...], + run_types=[...], # per-run KALIGN_MATRIX_* constants + format="fasta", + seq_type=1, # fallback matrix if run_types empty + seed=42, # base seed (run k gets seed+k) + min_support=0, # POAR consensus threshold + refine=0, # KALIGN_REFINE_* constant + vsm_amax=-1.0, # -1.0 = C default + realign=0, + seq_weights=-1.0, # -1.0 = C default + n_threads=1, + consistency_anchors=0, + consistency_weight=2.0, +) +``` + +This is the ONLY way to pass fine-grained per-run parameters. It is used +exclusively by the optimizer, not by benchmark runners. + +### Scoring + +```python +import kalign + +# SP score (0-100) +sp = kalign.compare("reference.fasta", "test.fasta") + +# Detailed: recall, precision, F1, TC +d = kalign.compare_detailed("reference.fasta", "test.fasta", max_gap_frac=-1.0) +# d = {"recall": ..., "precision": ..., "f1": ..., "tc": ..., ...} + +# With BAliBASE XML column mask +d = kalign.compare_detailed("ref.fasta", "test.fasta", column_mask=[1,0,1,...]) +``` + +--- + +## Step 1: Update `src/runners.py` — `run_kalign()` + +### 1a. Simplify `run_kalign()` + +Remove all deprecated parameter kwargs. The function should only accept +`mode` and `seq_type`: + +```python +def run_kalign( + input_fasta: Path, + mode: str = "default", + seq_type: str = "auto", +) -> AlignResult: + """Run kalign via the Python API using mode presets.""" + import kalign as _kalign + + start = time.perf_counter() + result = _kalign.align_from_file( + str(input_fasta), + seq_type=seq_type, + mode=mode, + ) + wall = time.perf_counter() - start + ... +``` + +### 1b. Update `METHODS` registry + +```python +METHODS = { + "kalign": { + "fn": run_kalign, + "mode": "default", + }, + "kalign_fast": { + "fn": run_kalign, + "mode": "fast", + }, + "kalign_accurate": { + "fn": run_kalign, + "mode": "accurate", + }, + # External tools unchanged + "mafft": {"fn": run_mafft}, + "muscle": {"fn": run_muscle}, + "clustalo": {"fn": run_clustalo}, +} +``` + +Remove `"kalign_precise"` — use `"kalign_accurate"` instead. (If you +need backward compatibility with existing result files, keep `"precise"` +as an alias that maps to `mode="accurate"`.) + +### 1c. Update `METHOD_COLORS` and `METHOD_ORDER` + +Replace `"kalign_precise"` with `"kalign_accurate"` everywhere. + +### 1d. Update `run_method()` dispatcher + +After simplifying `run_kalign()`, the dispatcher should pass only +`mode` and `seq_type`. + +**Smoke test**: +```bash +uv run python -c " +from src.runners import run_method +from pathlib import Path +r = run_method('kalign_fast', Path('data/downloads/balibase/bb3_release/RV11/BB11001.tfa'), Path('/tmp/test')) +print(f'wall_time={r.wall_time:.2f}s') +" +``` + +--- + +## Step 2: Update `config/config.yaml` + +Replace `kalign_precise` with `kalign_accurate`: + +```yaml +methods: + kalign: + mode: default + kalign_fast: + mode: fast + kalign_accurate: + mode: accurate + mafft: {} + muscle: {} + clustalo: {} +``` + +Find-and-replace `kalign_precise` → `kalign_accurate` in: +- `config/config.yaml` +- Any Snakemake rules that reference method names + +--- + +## Step 3: Update optimizer imports (in kalign repo) + +**Note**: This is in `/Users/timo/code/kalign/benchmarks/`, not the +benchmark repo. Listed here for completeness. + +### 3a. Fix `matrix_map_int` + +```python +from kalign._core import MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66 + +"matrix_map_int": [MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66], +"matrix_map_str": ["pfasum43", "pfasum60", "corblosum66"], +``` + +### 3b. Keep `consistency` and `consistency_weight` in search space + +These are real per-run parameters passed through +`ensemble_custom_file_to_file()` → `kalign_run_config` → C engine. + +### 3c. Export JSON format for optimized presets + +```json +{ + "protein": { + "fast": { + "n_runs": 1, + "min_support": 0, + "runs": [{ + "matrix": "pfasum60", + "gpo": 8.4087, "gpe": 0.5153, "tgpe": 0.4927, + "vsm_amax": 1.448, "seq_weights": 1.063, + "dist_scale": 0.0, "realign": 0, "refine": 0, + "adaptive_budget": 0, "tree_seed": 42, "tree_noise": 0.1623, + "consistency_anchors": 0, "consistency_weight": 2.0 + }] + } + } +} +``` + +--- + +## Step 4: Run benchmark to verify + +```bash +cd /Users/timo/work/Documents/Manuscripts/2026_kalign_35 + +# Quick test +podman run --rm -v $(pwd):/work -w /work kalign-35-bench \ + snakemake --cores 4 results/summaries/alignment_accuracy.json +``` + +Or on host: +```bash +uv run python workflow/scripts/run_balibase_quick.py +``` + +Expected results for protein (BAliBASE, 218 families): +- `kalign` (default mode): F1 ~ 0.72-0.73, TC ~ 0.47 +- `kalign_fast`: F1 ~ 0.70-0.72 +- `kalign_accurate`: F1 ~ 0.76-0.77 + +--- + +## Step 5: Update figure scripts (cosmetic) + +Replace "precise" → "accurate" in labels and legends: +- `figures/fig_balibase.py` +- `figures/fig_bralibase.py` +- `figures/fig_speed.py` +- `figures/fig_summary_heatmap.py` +- `figures/style.py` + +--- + +## Implementation Order + +1. **Step 1** (runners.py) — critical, unblocks everything +2. **Step 2** (config.yaml) — quick, do with step 1 +3. **Step 3** (optimizer) — independent, in kalign repo +4. **Step 4** (verify) — after steps 1-2 +5. **Step 5** (figures) — cosmetic, can wait + +Steps 1-2 are in the benchmark repo. +Step 3 is in the kalign repo. +Steps 4-5 are in the benchmark repo. + +--- + +## Risk: Container Rebuild + +The benchmark container has kalign baked in. After the update, rebuild: + +```bash +cd /Users/timo/work/Documents/Manuscripts/2026_kalign_35 +bash containers/build.sh +``` + +Or for host-side testing: +```bash +cd /Users/timo/code/kalign && uv pip install -e . +``` diff --git a/docs/parameter-cleanup-integration.md b/docs/parameter-cleanup-integration.md new file mode 100644 index 0000000..6876e6e --- /dev/null +++ b/docs/parameter-cleanup-integration.md @@ -0,0 +1,337 @@ +# Kalign Python API: Integration Guide + +This document describes kalign's Python API architecture and how to use +it from benchmark runners and the NSGA-III optimizer. + +--- + +## 1. Architecture: Two Paths + +Kalign's Python bindings have exactly two code paths: + +### Path 1: Mode-based (public API) + +For standard alignment using preset configurations: + +```python +import kalign + +# In-memory +result = kalign.align(sequences, mode="default") +result = kalign.align(sequences, mode="fast") +result = kalign.align(sequences, mode="accurate") + +# File-based (returns AlignedSequences with names + sequences) +result = kalign.align_from_file("input.fasta", mode="default") + +# File-to-file +kalign.align_file_to_file("input.fasta", "output.fasta", mode="default") +``` + +These call `_core.align_mode()` / `_core.align_from_file_mode()` / +`_core.align_file_to_file_mode()`, which delegate to the C function +`kalign_get_mode_preset()` → `kalign_align_full()`. + +**Mode presets** are biotype-aware. Each mode × biotype (protein / DNA / RNA) +returns fully concrete per-run configurations optimized by NSGA-III (protein) +or using sensible defaults (DNA/RNA). + +**Gap penalty override rule**: If any gap penalty (`gap_open`, `gap_extend`, +`terminal_gap_extend`) is set, mode is forced to `"fast"` and the specified +penalties replace the preset values. Value `-1.0` means "use preset default". + +### Path 2: Optimizer (internal API) + +For the NSGA-III optimizer that needs full per-run control: + +```python +from kalign._core import ensemble_custom_file_to_file + +ensemble_custom_file_to_file( + input_file="input.fasta", + output_file="output.fasta", + run_gpo=[8.4, 6.2, 7.1], # per-run gap open + run_gpe=[0.5, 1.0, 0.8], # per-run gap extend + run_tgpe=[0.5, 0.9, 0.6], # per-run terminal gap extend + run_noise=[0.16, 0.25, 0.10], # per-run tree noise sigma + run_types=[1, 2, 3], # per-run matrix constants + format="fasta", + seq_type=1, # KALIGN_MATRIX_PFASUM43 + seed=42, # base seed (run k gets seed+k) + min_support=0, # POAR consensus threshold + refine=2, # KALIGN_REFINE_CONFIDENT + vsm_amax=2.0, # shared across runs + realign=1, # shared across runs + seq_weights=1.0, # shared across runs + n_threads=4, + consistency_anchors=0, # shared across runs + consistency_weight=2.0, # shared across runs +) +``` + +This builds a `kalign_run_config[]` array and calls `kalign_align_full()`. + +--- + +## 2. C Entry Point + +```c +int kalign_align_full(struct msa *msa, + const struct kalign_run_config *runs, + int n_runs, + const struct kalign_ensemble_config *ens, + int n_threads); +``` + +Single entry point for all alignment. Receives fully concrete configs +(no sentinel values) and executes them. + +### `kalign_run_config` (14 fields) + +```c +struct kalign_run_config { + int matrix; /* KALIGN_MATRIX_* constant */ + float gpo; /* gap open penalty */ + float gpe; /* gap extend penalty */ + float tgpe; /* terminal gap extend penalty */ + float vsm_amax; /* variable scoring matrix amp (0=off) */ + float seq_weights; /* profile rebalancing (0=off) */ + float dist_scale; /* distance-dependent gap scaling */ + int refine; /* KALIGN_REFINE_* constant */ + int adaptive_budget; /* scale refinement by uncertainty */ + int realign; /* iterative tree-rebuild iters (0=off) */ + uint64_t tree_seed; /* random seed for tree perturbation */ + float tree_noise; /* tree perturbation sigma (0.0=none) */ + int consistency_anchors; /* anchor consistency rounds (0=off) */ + float consistency_weight; /* anchor consistency bonus (default 2) */ +}; +``` + +### `kalign_ensemble_config` + +```c +struct kalign_ensemble_config { + int min_support; /* POAR consensus threshold (0=auto) */ +}; +``` + +### Mode presets + +```c +int kalign_get_mode_preset(const char *mode, /* "fast"/"default"/"accurate" */ + int biotype, /* ALN_BIOTYPE_PROTEIN or _DNA */ + struct kalign_run_config *runs, /* out: array[8] */ + int *n_runs, /* out */ + struct kalign_ensemble_config *ens); /* out */ +``` + +--- + +## 3. Matrix Constants + +| Constant | Value | Description | +|---------------------------|-------|---------------------| +| `KALIGN_MATRIX_AUTO` | 0 | Auto-detect | +| `KALIGN_MATRIX_PFASUM43` | 1 | PFASUM43 (protein) | +| `KALIGN_MATRIX_PFASUM60` | 2 | PFASUM60 (protein) | +| `KALIGN_MATRIX_CORBLOSUM66`| 3 | CorBLOSUM66 (protein) | +| `KALIGN_MATRIX_DNA` | 5 | DNA | +| `KALIGN_MATRIX_DNA_INTERNAL`| 6 | DNA internal | +| `KALIGN_MATRIX_RNA` | 7 | RNA | + +Python access: +```python +from kalign._core import ( + MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66, + MATRIX_DNA, MATRIX_DNA_INTERNAL, MATRIX_RNA, MATRIX_AUTO, +) +``` + +Legacy names (`PROTEIN`, `DNA`, etc.) still work as aliases. + +### Critical bug fix: value 3 was GONNET, now CorBLOSUM66 + +Old code used `PROTEIN` (value 3) thinking it was a generic protein +matrix, but it actually triggered the GONNET scoring path (gon250: +scores ~0-200). This is now `MATRIX_CORBLOSUM66` (CorBLOSUM66, +1/3-bit units, scores -4 to 13). + +The correct three-matrix mapping for protein optimization: + +```python +from kalign._core import MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66 + +matrix_map_int = [MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66] +matrix_map_str = ["pfasum43", "pfasum60", "corblosum66"] +``` + +--- + +## 4. Python API Signatures + +### `kalign.align()` + +```python +kalign.align( + sequences: list[str], + seq_type: str | int = "auto", # "auto", "dna", "rna", "protein" + gap_open: float | None = None, # overrides mode → forces "fast" + gap_extend: float | None = None, + terminal_gap_extend: float | None = None, + n_threads: int | None = None, + mode: str | None = None, # "fast", "default", "accurate" + fmt: str = "plain", # "plain", "biopython", "skbio" + ids: list[str] | None = None, +) -> list[str] +``` + +### `kalign.align_from_file()` + +```python +kalign.align_from_file( + input_file: str, + seq_type: str | int = "auto", + gap_open: float | None = None, + gap_extend: float | None = None, + terminal_gap_extend: float | None = None, + n_threads: int | None = None, + mode: str | None = None, +) -> AlignedSequences # unpacks as (names, sequences) +``` + +### `kalign.align_file_to_file()` + +```python +kalign.align_file_to_file( + input_file: str, + output_file: str, + format: str = "fasta", + seq_type: str | int = "auto", + gap_open: float | None = None, + gap_extend: float | None = None, + terminal_gap_extend: float | None = None, + n_threads: int | None = None, + mode: str | None = None, +) +``` + +### `_core.ensemble_custom_file_to_file()` (optimizer only) + +```python +from kalign._core import ensemble_custom_file_to_file + +ensemble_custom_file_to_file( + input_file: str, + output_file: str, + run_gpo: list[float], # per-run + run_gpe: list[float], # per-run + run_tgpe: list[float], # per-run + run_noise: list[float], # per-run + run_types: list[int] = [], # per-run matrix constants + format: str = "fasta", + seq_type: int = 1, # fallback if run_types empty + seed: int = 42, # base seed (run k → seed+k) + min_support: int = 0, # POAR threshold + refine: int = 0, # KALIGN_REFINE_* constant + vsm_amax: float = -1.0, # -1.0 = C default + realign: int = 0, + seq_weights: float = -1.0, # -1.0 = C default + n_threads: int = 1, + consistency_anchors: int = 0, + consistency_weight: float = 2.0, +) +``` + +--- + +## 5. Optimizer Search Space → `kalign_run_config` Mapping + +``` +Per-run parameter | optimizer field | run_config field +----------------------|----------------------|------------------ +gap open | run_gpo[k] | .gpo +gap extend | run_gpe[k] | .gpe +terminal gap extend | run_tgpe[k] | .tgpe +tree noise | run_noise[k] | .tree_noise +matrix type | run_types[k] | .matrix +tree seed | seed + k | .tree_seed +VSM amplitude | vsm_amax | .vsm_amax +seq weights | seq_weights | .seq_weights +realign iters | realign | .realign +refinement mode | refine | .refine +consistency anchors | consistency_anchors | .consistency_anchors +consistency weight | consistency_weight | .consistency_weight +adaptive budget | (not searched) | .adaptive_budget = 0 +dist scale | (not searched) | .dist_scale = 0.0 +``` + +Ensemble-level: +``` +min_support | min_support | ens.min_support +``` + +--- + +## 6. Refinement Constants + +```python +from kalign._core import REFINE_NONE, REFINE_ALL, REFINE_CONFIDENT, REFINE_INLINE +``` + +| Constant | Value | Description | +|-------------------|-------|----------------------------------------| +| `REFINE_NONE` | 0 | No refinement | +| `REFINE_ALL` | 1 | Refine all columns | +| `REFINE_CONFIDENT` | 2 | Refine only high-confidence columns | +| `REFINE_INLINE` | 3 | Inline refinement during alignment | + +--- + +## 7. Matrix Default Penalties + +| Matrix | gpo | gpe | tgpe | Score range | +|---------------|--------|-------|--------|---------------| +| PFASUM43 | 7.0 | 1.25 | 1.0 | -6 to 13 | +| PFASUM60 | 7.0 | 1.25 | 1.0 | -6 to 14 | +| CorBLOSUM66 | 5.5 | 2.0 | 1.0 | -4 to 13 | +| DNA | 8.0 | 6.0 | 0.0 | -4 to 5 | +| DNA_INTERNAL | 8.0 | 6.0 | 8.0 | -4 to 5 | +| RNA | 217.0 | 39.4 | 292.6 | ~160 to 383 | + +All three protein matrices are in 1/3-bit units with directly comparable +gap penalty ranges (optimizer searches `[2.0, 15.0]` for gpo across all three). + +--- + +## 8. How Presets Get Into the C Library + +Optimized presets are hardcoded in `lib/src/aln_wrap.c` in the functions +`preset_protein()`, `preset_dna()`, and `preset_rna()`. + +Each function handles all three modes (fast/default/accurate) and fills +the caller-provided `runs[]` array and `*n_runs` count. + +To update presets after re-optimization: +1. Run the optimizer to get the Pareto front +2. Select the fast/default/accurate configurations +3. Export to JSON: + ```json + { + "protein": { + "fast": { + "n_runs": 1, + "min_support": 0, + "runs": [{ + "matrix": "pfasum60", + "gpo": 8.4087, "gpe": 0.5153, "tgpe": 0.4927, + "vsm_amax": 1.448, "seq_weights": 1.063, + "dist_scale": 0.0, "realign": 0, "refine": 0, + "adaptive_budget": 0, "tree_seed": 42, "tree_noise": 0.1623, + "consistency_anchors": 0, "consistency_weight": 2.0 + }] + } + } + } + ``` +4. Translate JSON → C `preset_run()` calls in `aln_wrap.c` +5. Rebuild and test diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index d9635ac..723ee18 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -16,16 +16,29 @@ #endif #endif -#define KALIGN_TYPE_DNA 0 -#define KALIGN_TYPE_DNA_INTERNAL 1 -#define KALIGN_TYPE_RNA 2 -#define KALIGN_TYPE_PROTEIN 3 -#define KALIGN_TYPE_PROTEIN_DIVERGENT 4 -#define KALIGN_TYPE_PROTEIN_PFASUM43 5 -#define KALIGN_TYPE_PROTEIN_PFASUM60 6 -#define KALIGN_TYPE_PROTEIN_PFASUM_AUTO 7 -#define KALIGN_TYPE_UNDEFINED 8 -#define KALIGN_TYPE_PROTEIN_CORBLOSUM66 9 +/* Substitution matrix constants. + Every value maps to exactly one scoring table. No duplicates. + Value 4 is reserved for legacy GONNET (KALIGN_TYPE_PROTEIN_DIVERGENT). */ +#define KALIGN_MATRIX_AUTO 0 /* auto-select for biotype */ +#define KALIGN_MATRIX_PFASUM43 1 /* 1/3 bit, divergent protein */ +#define KALIGN_MATRIX_PFASUM60 2 /* 1/3 bit, moderate protein */ +#define KALIGN_MATRIX_CORBLOSUM66 3 /* 1/3 bit, close protein */ +#define KALIGN_MATRIX_DNA 5 /* DNA match/mismatch (+5/-4) */ +#define KALIGN_MATRIX_DNA_INTERNAL 6 /* DNA internal (tgpe=8) */ +#define KALIGN_MATRIX_RNA 7 /* RNA RIBOSUM-like (~160-383) */ + +/* Backward compatibility — old KALIGN_TYPE_* map to KALIGN_MATRIX_*. + KALIGN_TYPE_PROTEIN_DIVERGENT stays at 4 (GONNET, dead code). */ +#define KALIGN_TYPE_DNA KALIGN_MATRIX_DNA +#define KALIGN_TYPE_DNA_INTERNAL KALIGN_MATRIX_DNA_INTERNAL +#define KALIGN_TYPE_RNA KALIGN_MATRIX_RNA +#define KALIGN_TYPE_PROTEIN KALIGN_MATRIX_PFASUM43 +#define KALIGN_TYPE_PROTEIN_DIVERGENT 4 /* GONNET — dead code, do not use */ +#define KALIGN_TYPE_PROTEIN_PFASUM43 KALIGN_MATRIX_PFASUM43 +#define KALIGN_TYPE_PROTEIN_PFASUM60 KALIGN_MATRIX_PFASUM60 +#define KALIGN_TYPE_PROTEIN_PFASUM_AUTO KALIGN_MATRIX_AUTO +#define KALIGN_TYPE_UNDEFINED KALIGN_MATRIX_AUTO +#define KALIGN_TYPE_PROTEIN_CORBLOSUM66 KALIGN_MATRIX_CORBLOSUM66 #define KALIGN_REFINE_NONE 0 #define KALIGN_REFINE_ALL 1 @@ -139,22 +152,25 @@ EXTERN int kalign_generate_ensemble_runs(const struct kalign_run_config* base, int n_runs, uint64_t seed, struct kalign_run_config* out); -/* Get a built-in mode preset (protein only). +/* Get a built-in mode preset. * * Presets were derived from NSGA-III multi-objective optimization * (objectives: F1, TC, wall_time) with 5-fold cross-validation on - * BAliBASE v4. + * BAliBASE v4 (protein) and BRAliBASE (RNA). * - * mode: "fast", "default", or "accurate" (case-insensitive). - * NULL is treated as "default". - * runs: caller-allocated array of at least KALIGN_MAX_PRESET_RUNS configs. - * n_runs: filled with the number of runs in the preset. - * ens: filled with ensemble config (only meaningful when *n_runs > 1). + * mode: "fast", "default", or "accurate" (case-insensitive). + * NULL is treated as "default". + * biotype: ALN_BIOTYPE_PROTEIN, ALN_BIOTYPE_DNA, or ALN_BIOTYPE_RNA. + * Determines which preset grid slot to use. + * runs: caller-allocated array of at least KALIGN_MAX_PRESET_RUNS configs. + * n_runs: filled with the number of runs in the preset. + * ens: filled with ensemble config (only meaningful when *n_runs > 1). * * Returns 0 on success, -1 if mode is unknown. */ #define KALIGN_MAX_PRESET_RUNS 8 EXTERN int kalign_get_mode_preset(const char *mode, + int biotype, struct kalign_run_config *runs, int *n_runs, struct kalign_ensemble_config *ens); diff --git a/lib/include/kalign/kalign_config.h b/lib/include/kalign/kalign_config.h index 02b15f2..4924bdd 100644 --- a/lib/include/kalign/kalign_config.h +++ b/lib/include/kalign/kalign_config.h @@ -6,31 +6,29 @@ /* Per-run alignment configuration. Each field controls one aspect of a single alignment run. - Use kalign_run_config_defaults() to get a config with all sentinels/defaults, - then override only the fields you care about. */ + All values are concrete — no sentinel values. + Use kalign_run_config_defaults() for sensible PFASUM43 protein defaults. */ struct kalign_run_config { - int type; /* KALIGN_TYPE_* constant (UNDEFINED = auto-detect) */ - float gpo; /* gap open penalty (-1.0 = use matrix default) */ - float gpe; /* gap extend penalty (-1.0 = use matrix default) */ - float tgpe; /* terminal gap extend penalty (-1.0 = use matrix default) */ - float vsm_amax; /* variable scoring matrix amplitude (-1.0 = biotype default) */ - float dist_scale; /* distance-dependent gap scaling (0.0 = off) */ - float use_seq_weights; /* profile rebalancing pseudocount (-1.0 = biotype default) */ - int consistency_anchors; /* consistency transform: K anchor sequences (0 = off) */ - float consistency_weight; /* consistency transform: bonus scale (default: 2.0) */ - int refine; /* KALIGN_REFINE_* constant (default: NONE) */ - int adaptive_budget; /* scale refinement trials by uncertainty (0 = off) */ - int realign; /* iterative tree-rebuild iterations (0 = off) */ - uint64_t tree_seed; /* random seed for guide tree perturbation (0 = deterministic) */ - float tree_noise; /* guide tree perturbation sigma (0.0 = none) */ + int matrix; /* KALIGN_MATRIX_* constant (AUTO = auto-detect) */ + float gpo; /* gap open penalty */ + float gpe; /* gap extend penalty */ + float tgpe; /* terminal gap extend penalty */ + float vsm_amax; /* variable scoring matrix amplitude (0 = off) */ + float seq_weights; /* profile rebalancing pseudo-count (0 = off) */ + float dist_scale; /* distance-dependent gap scaling (0 = off) */ + int refine; /* KALIGN_REFINE_* constant (default: NONE) */ + int adaptive_budget; /* scale refinement trials by uncertainty (0=off)*/ + int realign; /* iterative tree-rebuild iterations (0 = off) */ + uint64_t tree_seed; /* random seed for guide tree perturbation */ + float tree_noise; /* guide tree perturbation sigma (0.0 = none) */ + int consistency_anchors; /* anchor consistency rounds (0 = off) */ + float consistency_weight; /* anchor consistency bonus weight (default: 2.0)*/ }; /* Ensemble orchestration configuration. Only used when n_runs > 1. */ struct kalign_ensemble_config { - uint64_t seed; /* base RNG seed for diversity generation */ - int min_support; /* POAR consensus threshold (0 = auto) */ - const char* save_poar; /* path to save POAR table (NULL = don't save) */ + int min_support; /* POAR consensus threshold (0 = auto) */ }; #endif diff --git a/lib/src/aln_param.c b/lib/src/aln_param.c index 2c25d4b..e3b9871 100644 --- a/lib/src/aln_param.c +++ b/lib/src/aln_param.c @@ -33,19 +33,20 @@ int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int } } if(biotype == ALN_BIOTYPE_DNA){ - /* include/kalign/ */ switch (type) { - case KALIGN_TYPE_DNA: + case KALIGN_MATRIX_DNA: set_subm_gaps_DNA(ap); break; - case KALIGN_TYPE_DNA_INTERNAL: + case KALIGN_MATRIX_DNA_INTERNAL: set_subm_gaps_DNA_internal(ap); break; - case KALIGN_TYPE_RNA: + case KALIGN_MATRIX_RNA: set_subm_gaps_RNA(ap); break; - case KALIGN_TYPE_PROTEIN: - ERROR_MSG("Detected DNA sequences but --type protein option was selected."); + case KALIGN_MATRIX_PFASUM43: + case KALIGN_MATRIX_PFASUM60: + case KALIGN_MATRIX_CORBLOSUM66: + ERROR_MSG("Detected DNA sequences but a protein matrix was selected."); break; default: set_subm_gaps_RNA(ap); @@ -53,29 +54,23 @@ int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int } }else if(biotype == ALN_BIOTYPE_PROTEIN){ switch (type) { - case KALIGN_TYPE_PROTEIN: + case KALIGN_MATRIX_PFASUM43: set_subm_gaps_PFASUM43(ap); break; - case KALIGN_TYPE_PROTEIN_DIVERGENT: - set_subm_gaps_gon250(ap); - break; - case KALIGN_TYPE_PROTEIN_PFASUM43: - set_subm_gaps_PFASUM43(ap); - break; - case KALIGN_TYPE_PROTEIN_PFASUM60: + case KALIGN_MATRIX_PFASUM60: set_subm_gaps_PFASUM60(ap); break; - case KALIGN_TYPE_PROTEIN_CORBLOSUM66: + case KALIGN_MATRIX_CORBLOSUM66: set_subm_gaps_CorBLOSUM66_13plus(ap); break; - case KALIGN_TYPE_DNA: - ERROR_MSG("Detected protein sequences but --type dna option was selected."); - break; - case KALIGN_TYPE_DNA_INTERNAL: - ERROR_MSG("Detected protein sequences but --type internal option was selected."); + case KALIGN_TYPE_PROTEIN_DIVERGENT: + /* Legacy GONNET path — retained for backward compat */ + set_subm_gaps_gon250(ap); break; - case KALIGN_TYPE_RNA: - ERROR_MSG("Detected protein sequences but --type rna option was selected."); + case KALIGN_MATRIX_DNA: + case KALIGN_MATRIX_DNA_INTERNAL: + case KALIGN_MATRIX_RNA: + ERROR_MSG("Detected protein sequences but a nucleotide matrix was selected."); break; default: set_subm_gaps_PFASUM43(ap); diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index bea07d5..6283ab5 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -29,23 +29,27 @@ #define ALN_WRAP_IMPORT #include "aln_wrap.h" -/* Resolve PFASUM_AUTO: pick PFASUM43 for divergent, PFASUM60 for closer. - Must be called after build_tree_kmeans which fills msa->seq_distances[]. */ -static int resolve_pfasum_auto(struct msa *msa, int *type) +/* Resolve KALIGN_MATRIX_AUTO into a concrete matrix constant. + For protein: use length-ratio heuristic (PFASUM43 vs PFASUM60). + For DNA/RNA: pick the standard matrix for that biotype. */ +static int resolve_matrix_auto(struct msa *msa, int *type) { int i; int min_len, max_len; float len_ratio; - if(*type != KALIGN_TYPE_PROTEIN_PFASUM_AUTO){ + if(*type != KALIGN_MATRIX_AUTO){ return OK; } - if(msa->biotype != ALN_BIOTYPE_PROTEIN){ - *type = KALIGN_TYPE_PROTEIN_PFASUM43; + + if(msa->biotype == ALN_BIOTYPE_DNA){ + /* DNA biotype covers both DNA and RNA sequences. + Default to RNA matrix (broader scoring range). */ + *type = KALIGN_MATRIX_RNA; return OK; } - /* Use sequence length ratio (max/min) to select matrix. + /* Protein: use sequence length ratio (max/min) to select matrix. Similar-length sequences (ratio < 1.5) prefer PFASUM43; high length variation (insertions/extensions) prefers PFASUM60. */ min_len = msa->sequences[0]->len; @@ -58,14 +62,14 @@ static int resolve_pfasum_auto(struct msa *msa, int *type) len_ratio = (min_len > 0) ? (float)max_len / (float)min_len : 1.0f; if(len_ratio < 1.5f){ - *type = KALIGN_TYPE_PROTEIN_PFASUM43; + *type = KALIGN_MATRIX_PFASUM43; }else{ - *type = KALIGN_TYPE_PROTEIN_PFASUM60; + *type = KALIGN_MATRIX_PFASUM60; } if(!msa->quiet){ LOG_MSG("Auto matrix: len_ratio=%.2f -> %s", len_ratio, - *type == KALIGN_TYPE_PROTEIN_PFASUM60 ? "PFASUM60" : "PFASUM43"); + *type == KALIGN_MATRIX_PFASUM60 ? "PFASUM60" : "PFASUM43"); } return OK; } @@ -182,7 +186,7 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, } /* Resolve auto matrix selection using BPM distances */ - RUN(resolve_pfasum_auto(msa, &type)); + RUN(resolve_matrix_auto(msa, &type)); /* align */ RUN(aln_param_init(&ap, @@ -304,7 +308,7 @@ int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } - RUN(resolve_pfasum_auto(msa, &type)); + RUN(resolve_matrix_auto(msa, &type)); RUN(aln_param_init(&ap, msa->biotype, @@ -402,7 +406,7 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } - RUN(resolve_pfasum_auto(msa, &type)); + RUN(resolve_matrix_auto(msa, &type)); RUN(aln_param_init(&ap, msa->biotype, @@ -559,7 +563,7 @@ int kalign_post_realign(struct msa *msa, int n_threads, int type, } /* seq_distances available from prior alignment */ - RUN(resolve_pfasum_auto(msa, &type)); + RUN(resolve_matrix_auto(msa, &type)); RUN(aln_param_init(&ap, msa->biotype, @@ -682,29 +686,27 @@ int kalign_post_realign(struct msa *msa, int n_threads, int type, struct kalign_run_config kalign_run_config_defaults(void) { struct kalign_run_config cfg; - cfg.type = KALIGN_TYPE_UNDEFINED; - cfg.gpo = -1.0f; - cfg.gpe = -1.0f; - cfg.tgpe = -1.0f; - cfg.vsm_amax = -1.0f; + cfg.matrix = KALIGN_MATRIX_PFASUM43; + cfg.gpo = 7.0f; /* PFASUM43 default */ + cfg.gpe = 1.25f; + cfg.tgpe = 1.0f; + cfg.vsm_amax = 2.0f; /* protein default */ + cfg.seq_weights = 0.0f; cfg.dist_scale = 0.0f; - cfg.use_seq_weights = -1.0f; - cfg.consistency_anchors = 0; - cfg.consistency_weight = 2.0f; cfg.refine = KALIGN_REFINE_NONE; cfg.adaptive_budget = 0; cfg.realign = 0; cfg.tree_seed = 0; cfg.tree_noise = 0.0f; + cfg.consistency_anchors = 0; + cfg.consistency_weight = 2.0f; return cfg; } struct kalign_ensemble_config kalign_ensemble_config_defaults(void) { struct kalign_ensemble_config ens; - ens.seed = 42; ens.min_support = 0; - ens.save_poar = NULL; return ens; } @@ -725,20 +727,20 @@ int kalign_align_full(struct msa* msa, /* Single-run path */ const struct kalign_run_config* r = &runs[0]; if(r->realign > 0){ - RUN(kalign_run_realign(msa, n_threads, r->type, + RUN(kalign_run_realign(msa, n_threads, r->matrix, r->gpo, r->gpe, r->tgpe, r->refine, r->adaptive_budget, r->dist_scale, r->vsm_amax, - r->realign, r->use_seq_weights, + r->realign, r->seq_weights, r->consistency_anchors, r->consistency_weight)); }else{ - RUN(kalign_run_seeded(msa, n_threads, r->type, + RUN(kalign_run_seeded(msa, n_threads, r->matrix, r->gpo, r->gpe, r->tgpe, r->refine, r->adaptive_budget, r->tree_seed, r->tree_noise, r->dist_scale, r->vsm_amax, - r->use_seq_weights, + r->seq_weights, r->consistency_anchors, r->consistency_weight)); } @@ -753,46 +755,41 @@ int kalign_align_full(struct msa* msa, /* NSGA-III optimized protein mode presets */ /* ======================================================================== */ -/* Helper: fill one run config with common preset values */ +/* Helper: fill one run config with preset values */ static void preset_run(struct kalign_run_config *r, - int type, float gpo, float gpe, float tgpe, + int matrix, float gpo, float gpe, float tgpe, float vsm_amax, float seq_weights, int realign, int refine, uint64_t seed, float noise) { *r = kalign_run_config_defaults(); - r->type = type; + r->matrix = matrix; r->gpo = gpo; r->gpe = gpe; r->tgpe = tgpe; r->vsm_amax = vsm_amax; - r->use_seq_weights = seq_weights; + r->seq_weights = seq_weights; r->realign = realign; r->refine = refine; - r->consistency_anchors = 0; - r->consistency_weight = 1.0f; r->tree_seed = seed; r->tree_noise = noise; } -int kalign_get_mode_preset(const char *mode, - struct kalign_run_config *runs, - int *n_runs, - struct kalign_ensemble_config *ens) -{ - const char *m = mode ? mode : "default"; - - *ens = kalign_ensemble_config_defaults(); +/* ---- Protein presets (NSGA-III optimized on BAliBASE v4) ---- */ +static int preset_protein(const char *m, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens) +{ if(strcasecmp(m, "fast") == 0){ *n_runs = 1; preset_run(&runs[0], - KALIGN_TYPE_PROTEIN_PFASUM60, - 8.4087f, 0.5153f, 0.4927f, /* gpo, gpe, tgpe */ - 1.448f, /* vsm_amax */ - 1.063f, /* seq_weights */ - 0, KALIGN_REFINE_NONE, /* realign, refine */ - 42, 0.1623f); /* seed, noise */ + KALIGN_MATRIX_PFASUM60, + 8.4087f, 0.5153f, 0.4927f, + 1.448f, 1.063f, + 0, KALIGN_REFINE_NONE, + 42, 0.1623f); return 0; } @@ -803,19 +800,22 @@ int kalign_get_mode_preset(const char *mode, int ra = 0; int ref = KALIGN_REFINE_NONE; - preset_run(&runs[0], KALIGN_TYPE_PROTEIN_DIVERGENT, + /* BUG FIX: these were labeled "gonnet" in the optimizer + but actually ran PFASUM43 (optimizer's matrix_map_int + mapped index 2 → KALIGN_TYPE_PROTEIN = PFASUM43). */ + preset_run(&runs[0], KALIGN_MATRIX_PFASUM43, 9.5703f, 0.6206f, 1.5751f, vsm, sw, ra, ref, 42, 0.063f); - preset_run(&runs[1], KALIGN_TYPE_PROTEIN_PFASUM43, + preset_run(&runs[1], KALIGN_MATRIX_PFASUM43, 5.6154f, 0.5469f, 1.0163f, vsm, sw, ra, ref, 43, 0.2828f); - preset_run(&runs[2], KALIGN_TYPE_PROTEIN_DIVERGENT, + preset_run(&runs[2], KALIGN_MATRIX_PFASUM43, 4.8979f, 1.3657f, 1.2367f, vsm, sw, ra, ref, 44, 0.4046f); - preset_run(&runs[3], KALIGN_TYPE_PROTEIN_DIVERGENT, + preset_run(&runs[3], KALIGN_MATRIX_PFASUM43, 7.244f, 0.9013f, 0.7332f, vsm, sw, ra, ref, 45, 0.3067f); - preset_run(&runs[4], KALIGN_TYPE_PROTEIN_PFASUM43, + preset_run(&runs[4], KALIGN_MATRIX_PFASUM43, 8.4354f, 1.8028f, 0.919f, vsm, sw, ra, ref, 46, 0.1964f); @@ -830,19 +830,19 @@ int kalign_get_mode_preset(const char *mode, int ra = 2; int ref = KALIGN_REFINE_NONE; - preset_run(&runs[0], KALIGN_TYPE_PROTEIN_DIVERGENT, + preset_run(&runs[0], KALIGN_MATRIX_PFASUM43, 13.1073f, 0.6667f, 0.613f, vsm, sw, ra, ref, 42, 0.3472f); - preset_run(&runs[1], KALIGN_TYPE_PROTEIN_PFASUM43, + preset_run(&runs[1], KALIGN_MATRIX_PFASUM43, 7.3036f, 0.6285f, 2.8521f, vsm, sw, ra, ref, 43, 0.2264f); - preset_run(&runs[2], KALIGN_TYPE_PROTEIN_PFASUM43, + preset_run(&runs[2], KALIGN_MATRIX_PFASUM43, 2.2452f, 2.0447f, 0.5878f, vsm, sw, ra, ref, 44, 0.1481f); - preset_run(&runs[3], KALIGN_TYPE_PROTEIN_PFASUM43, + preset_run(&runs[3], KALIGN_MATRIX_PFASUM43, 3.9617f, 0.8429f, 0.5156f, vsm, sw, ra, ref, 45, 0.4338f); - preset_run(&runs[4], KALIGN_TYPE_PROTEIN_PFASUM43, + preset_run(&runs[4], KALIGN_MATRIX_PFASUM43, 7.5402f, 1.8516f, 0.8772f, vsm, sw, ra, ref, 46, 0.1979f); @@ -852,3 +852,85 @@ int kalign_get_mode_preset(const char *mode, return -1; } + +/* ---- DNA/RNA preset stubs (use matrix defaults, to be optimized) ---- */ + +static int preset_dna(const char *m, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens) +{ + /* All DNA modes use a single run with standard DNA matrix defaults. + To be replaced with optimized presets after benchmarking. */ + (void)ens; + *n_runs = 1; + runs[0] = kalign_run_config_defaults(); + runs[0].matrix = KALIGN_MATRIX_DNA; + runs[0].gpo = 8.0f; + runs[0].gpe = 6.0f; + runs[0].tgpe = 0.0f; + runs[0].vsm_amax = 0.0f; + runs[0].seq_weights = 0.0f; + + if(strcasecmp(m, "fast") == 0){ + return 0; + } + if(strcasecmp(m, "default") == 0){ + return 0; + } + if(strcasecmp(m, "accurate") == 0){ + return 0; + } + return -1; +} + +static int preset_rna(const char *m, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens) +{ + /* All RNA modes use a single run with standard RNA matrix defaults. + To be replaced with optimized presets after benchmarking. */ + (void)ens; + *n_runs = 1; + runs[0] = kalign_run_config_defaults(); + runs[0].matrix = KALIGN_MATRIX_RNA; + runs[0].gpo = 217.0f; + runs[0].gpe = 39.4f; + runs[0].tgpe = 292.6f; + runs[0].vsm_amax = 0.0f; + runs[0].seq_weights = 0.0f; + + if(strcasecmp(m, "fast") == 0){ + return 0; + } + if(strcasecmp(m, "default") == 0){ + return 0; + } + if(strcasecmp(m, "accurate") == 0){ + return 0; + } + return -1; +} + +int kalign_get_mode_preset(const char *mode, + int biotype, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens) +{ + const char *m = mode ? mode : "default"; + + *ens = kalign_ensemble_config_defaults(); + + if(biotype == ALN_BIOTYPE_DNA){ + /* Detect RNA from matrix type if needed. + For now, DNA biotype covers both DNA and RNA. + Use RNA presets when RNA matrix was explicitly requested, + otherwise fall back to DNA presets. */ + return preset_dna(m, runs, n_runs, ens); + } + + /* Default: protein presets */ + return preset_protein(m, runs, n_runs, ens); +} diff --git a/lib/src/ensemble.c b/lib/src/ensemble.c index 6c806c5..bc70245 100644 --- a/lib/src/ensemble.c +++ b/lib/src/ensemble.c @@ -848,20 +848,20 @@ int kalign_ensemble_from_configs(struct msa* msa, } if(runs[k].realign > 0){ - RUN(kalign_run_realign(copy, n_threads, runs[k].type, + RUN(kalign_run_realign(copy, n_threads, runs[k].matrix, runs[k].gpo, runs[k].gpe, runs[k].tgpe, runs[k].refine, 0, runs[k].dist_scale, runs[k].vsm_amax, - runs[k].realign, runs[k].use_seq_weights, + runs[k].realign, runs[k].seq_weights, runs[k].consistency_anchors, runs[k].consistency_weight)); }else{ - RUN(kalign_run_seeded(copy, n_threads, runs[k].type, + RUN(kalign_run_seeded(copy, n_threads, runs[k].matrix, runs[k].gpo, runs[k].gpe, runs[k].tgpe, runs[k].refine, 0, runs[k].tree_seed, runs[k].tree_noise, runs[k].dist_scale, runs[k].vsm_amax, - runs[k].use_seq_weights, + runs[k].seq_weights, runs[k].consistency_anchors, runs[k].consistency_weight)); } @@ -892,13 +892,7 @@ int kalign_ensemble_from_configs(struct msa* msa, LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); } - /* Save POAR table if requested */ - if(ens != NULL && ens->save_poar != NULL){ - RUN(poar_table_write(poar, ens->save_poar)); - if(!msa->quiet){ - LOG_MSG(" Saved POAR table to %s", ens->save_poar); - } - } + /* POAR save removed from ensemble_config — debug feature */ /* Determine min_support */ int min_support = (ens != NULL) ? ens->min_support : 0; @@ -952,13 +946,13 @@ int kalign_ensemble_from_configs(struct msa* msa, /* Post-selection refinement always uses REFINE_CONFIDENT and the winning run's parameters (matching old behavior). */ - RUN(kalign_run_seeded(copy, n_threads, runs[best_k].type, + RUN(kalign_run_seeded(copy, n_threads, runs[best_k].matrix, runs[best_k].gpo, runs[best_k].gpe, runs[best_k].tgpe, KALIGN_REFINE_CONFIDENT, 0, runs[best_k].tree_seed, runs[best_k].tree_noise, runs[best_k].dist_scale, runs[best_k].vsm_amax, - runs[best_k].use_seq_weights, + runs[best_k].seq_weights, runs[best_k].consistency_anchors, runs[best_k].consistency_weight)); diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index af3f694..02fefd0 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -7,6 +7,7 @@ import os import threading +import warnings from importlib import import_module from typing import Any, List, Literal, Optional, Union @@ -70,7 +71,7 @@ def __repr__(self): PROTEIN_CORBLOSUM66 = _core.PROTEIN_CORBLOSUM66 AUTO = _core.AUTO -# Refinement mode constants +# Refinement mode constants (used by ensemble_custom_file_to_file optimizer path) REFINE_NONE = _core.REFINE_NONE REFINE_ALL = _core.REFINE_ALL REFINE_CONFIDENT = _core.REFINE_CONFIDENT @@ -82,53 +83,57 @@ def __repr__(self): MODE_ACCURATE = "accurate" MODE_PRECISE = "precise" # deprecated alias for "accurate" -# Named modes with NSGA-III optimized protein presets. -# Each mode is a Pareto-optimal configuration trading accuracy vs speed, -# derived from multi-objective optimization on BAliBASE v4 (218 families). -# -# When mode is one of these AND no explicit gap/matrix overrides are provided, -# the C-side kalign_get_mode_preset() function is used directly, which sets -# per-run heterogeneous gap penalties and scoring matrices. +# Valid preset modes (resolved by C library) _PRESET_MODES = {"fast", "default", "accurate"} -# Legacy mode presets (used when explicit param overrides are present). -# Values match C-side kalign_get_mode_preset() v2 from NSGA-III optimization. -_MODE_PRESETS = { - "fast": { - "vsm_amax": 1.448, - "seq_weights": 1.063, - "consistency": 0, - "consistency_weight": 1.724, - }, - "default": { - "vsm_amax": 1.885, - "seq_weights": 0.592, - "ensemble": 5, - "consistency": 0, - "consistency_weight": 0.638, - }, - "accurate": { - "vsm_amax": 1.682, - "seq_weights": 1.48, - "ensemble": 5, - "realign": 2, - "consistency": 0, - "consistency_weight": 1.317, - }, - "precise": { # deprecated alias - "vsm_amax": 1.682, - "seq_weights": 1.48, - "ensemble": 5, - "realign": 2, - "consistency": 0, - "consistency_weight": 1.317, - }, -} - # Global thread control _thread_local = threading.local() _default_threads = 1 +# Sequence type string->int mapping (shared by all entry points) +_SEQ_TYPE_MAP = { + "auto": AUTO, + "dna": DNA, + "rna": RNA, + "protein": PROTEIN, + "pfasum43": PROTEIN_PFASUM43, + "pfasum60": PROTEIN_PFASUM60, + "pfasum": PROTEIN_PFASUM_AUTO, + "divergent": PROTEIN_DIVERGENT, + "internal": DNA_INTERNAL, +} + + +def _resolve_seq_type(seq_type): + """Convert string or int seq_type to integer constant.""" + if isinstance(seq_type, int): + return seq_type + lower = seq_type.lower() + if lower not in _SEQ_TYPE_MAP: + raise ValueError( + f"Invalid seq_type: {seq_type}. Must be one of: {list(_SEQ_TYPE_MAP.keys())}" + ) + return _SEQ_TYPE_MAP[lower] + + +def _resolve_mode_name(mode): + """Normalize mode name, handling 'precise' -> 'accurate' alias.""" + if mode is None: + return "default" + lower = mode.lower() + if lower == "precise": + warnings.warn( + 'mode="precise" is deprecated, use mode="accurate" instead.', + DeprecationWarning, + stacklevel=3, + ) + return "accurate" + if lower not in _PRESET_MODES: + raise ValueError( + f"Invalid mode: {mode!r}. Must be one of: 'default', 'fast', 'accurate'" + ) + return lower + def _conf_to_pp(conf: float) -> str: """Convert a confidence value [0..1] to HMMER-style PP character.""" @@ -138,10 +143,7 @@ def _conf_to_pp(conf: float) -> str: def _confidence_to_pp_string(seq: str, confidences: list) -> str: - """Convert per-residue confidence array to PP string. - - Gap positions get '.', residues get HMMER-style PP characters. - """ + """Convert per-residue confidence array to PP string.""" pp = [] for ch, conf in zip(seq, confidences): if ch == "-" or ch == ".": @@ -151,112 +153,40 @@ def _confidence_to_pp_string(seq: str, confidences: list) -> str: return "".join(pp) -def align( - sequences: List[str], - seq_type: Union[str, int] = "auto", - gap_open: Optional[float] = None, - gap_extend: Optional[float] = None, - terminal_gap_extend: Optional[float] = None, - n_threads: Optional[int] = None, - refine: Union[str, int] = "none", - ensemble: int = 0, - min_support: int = 0, - seq_weights: float = 0.0, - consistency: int = 5, - consistency_weight: float = 2.0, - vsm_amax: float = -1.0, - realign: int = 0, - ensemble_seed: int = 42, - mode: Optional[str] = None, - fmt: Literal["plain", "biopython", "skbio"] = "plain", - ids: Optional[List[str]] = None, -) -> Union[List[str], Any]: - """ - Multiple sequence alignment via Kalign. +def _parse_refine_mode(refine): + """Convert string or int refine mode to integer constant.""" + if isinstance(refine, int): + return refine + refine_map = { + "none": REFINE_NONE, + "all": REFINE_ALL, + "confident": REFINE_CONFIDENT, + "inline": REFINE_INLINE, + } + refine_lower = refine.lower() + if refine_lower not in refine_map: + raise ValueError( + f"Invalid refine mode: {refine}. Must be one of: {list(refine_map.keys())}" + ) + return refine_map[refine_lower] - Parameters - ---------- - sequences : list of str - List of sequences to align. Sequences should be provided as strings - containing the sequence characters (e.g., 'ATCG' for DNA, 'ACGU' for RNA, - or amino acid codes for proteins). - seq_type : str or int, optional - Sequence type specification. Can be: - - "auto" or AUTO: Auto-detect sequence type (default) - - "dna" or DNA: DNA sequences - - "rna" or RNA: RNA sequences - - "protein" or PROTEIN: Protein sequences - - "divergent" or PROTEIN_DIVERGENT: Divergent protein sequences - - "internal" or DNA_INTERNAL: DNA with internal gap preference - gap_open : float, optional - Gap opening penalty (positive value, e.g. 5.5). If None, uses Kalign defaults. - gap_extend : float, optional - Gap extension penalty (positive value, e.g. 2.0). If None, uses Kalign defaults. - terminal_gap_extend : float, optional - Terminal gap extension penalty (positive value, e.g. 1.0). If None, uses Kalign defaults. - n_threads : int, optional - Number of threads to use for alignment. If None, uses global default. - refine : str or int, optional - Refinement mode: "none", "all", "confident", or "inline" (default: "none"). - ensemble : int, optional - Number of ensemble runs (default: 0 = off). Set to e.g. 3 for higher - accuracy at the cost of ~10x runtime. - vsm_amax : float, optional - Variable scoring matrix amplitude (default: -1.0 = use kalign defaults: - 2.0 for protein, 0.0 for DNA/RNA). Set to 0.0 to disable. - realign : int, optional - Number of alignment-guided tree rebuild iterations (default: 0 = off). - ensemble_seed : int, optional - RNG seed for ensemble runs (default: 42). - mode : str, optional - Preset mode: "default" (consistency+VSM), "fast" (VSM only), - "precise" (ensemble+VSM+realign). Explicit parameters override - mode defaults. None is treated as "default". - fmt : {'plain', 'biopython', 'skbio'}, default 'plain' - Choose return-object flavour: - - 'plain': list of aligned sequences (fastest) - - 'biopython': Bio.Align.MultipleSeqAlignment object - - 'skbio': skbio.TabularMSA object - ids : list of str, optional - Sequence IDs (used only for Biopython / scikit-bio objects). - If None, generates 'seq0', 'seq1', etc. - Returns - ------- - list of str | Bio.Align.MultipleSeqAlignment | skbio.TabularMSA - Aligned sequences. Return type depends on `fmt` parameter. - - Raises - ------ - ValueError - If input sequences are empty or invalid - RuntimeError - If alignment fails - ImportError - If Biopython or scikit-bio are requested but not installed - - Examples - -------- - >>> import kalign - >>> sequences = ["ATCGATCGATCG", "ATCGTCGATCG", "ATCGATCATCG"] - - # 1) Plain list (default) - >>> aligned = kalign.align(sequences) - >>> print(aligned) - ['ATCGATCGATCG', 'ATCG-TCGATCG', 'ATCGATC-ATCG'] - - # 2) Biopython object - >>> aln_bp = kalign.align(sequences, fmt="biopython", ids=["s1","s2","s3"]) - >>> print(type(aln_bp)) - - - # 3) scikit-bio object - >>> aln_sk = kalign.align(sequences, fmt="skbio") - >>> print(type(aln_sk)) - - """ +def _infer_skbio_type(sequences, skbio_seq): + """Infer the appropriate skbio sequence class from raw sequence content.""" + chars = set() + for seq in sequences: + chars.update(seq.upper().replace("-", "").replace(".", "")) + dna_chars = set("ACGTNRYSWKMBDHV") + rna_chars = set("ACGUNRYSWKMBDHV") + if "U" in chars and "T" not in chars and chars <= rna_chars: + return skbio_seq.RNA + if chars <= dna_chars: + return skbio_seq.DNA + return skbio_seq.Protein - # Input validation - replicate C CLI robustness + +def _validate_sequences(sequences): + """Validate input sequences, raising on empty/invalid input.""" if not sequences: raise ValueError("No sequences were found in the input") @@ -268,7 +198,6 @@ def align( if not all(isinstance(seq, str) for seq in sequences): raise ValueError("All sequences must be strings") - # Check for empty or whitespace-only sequences empty_sequences = [] for i, seq in enumerate(sequences): if not seq or not seq.strip(): @@ -284,22 +213,16 @@ def align( f"Sequences at indices {empty_sequences} are empty or contain only whitespace" ) - # Check for valid sequence characters (basic validation) for i, seq in enumerate(sequences): - # Remove common whitespace and check if anything remains cleaned_seq = "".join(seq.split()) if len(cleaned_seq) == 0: raise ValueError( f"Sequence at index {i} contains only whitespace characters" ) - - # Check for obviously invalid characters (control characters, etc) if any(ord(char) < 32 for char in cleaned_seq if char not in "\t\n\r"): raise ValueError( f"Sequence at index {i} contains invalid control characters" ) - - # Check for digits and other problematic characters that cause platform-specific segfaults invalid_chars = set(char for char in cleaned_seq if char.isdigit()) if invalid_chars: raise ValueError( @@ -307,67 +230,79 @@ def align( f"Sequences should only contain valid biological sequence characters." ) - # Warn about very short sequences (like C CLI warnings) very_short_sequences = [ i for i, seq in enumerate(sequences) if len(seq.strip()) < 3 ] if very_short_sequences and len(very_short_sequences) > len(sequences) * 0.5: - import warnings - warnings.warn( - f"Many sequences are very short (< 3 characters). This may affect alignment quality.", + "Many sequences are very short (< 3 characters). This may affect alignment quality.", UserWarning, - stacklevel=2, + stacklevel=3, ) - # Convert string sequence types to integers - seq_type_map = { - "auto": AUTO, - "dna": DNA, - "rna": RNA, - "protein": PROTEIN, - "pfasum43": PROTEIN_PFASUM43, - "pfasum60": PROTEIN_PFASUM60, - "pfasum": PROTEIN_PFASUM_AUTO, - "divergent": PROTEIN_DIVERGENT, - "internal": DNA_INTERNAL, - } - if isinstance(seq_type, str): - seq_type_lower = seq_type.lower() - if seq_type_lower not in seq_type_map: +def align( + sequences: List[str], + seq_type: Union[str, int] = "auto", + gap_open: Optional[float] = None, + gap_extend: Optional[float] = None, + terminal_gap_extend: Optional[float] = None, + n_threads: Optional[int] = None, + mode: Optional[str] = None, + fmt: Literal["plain", "biopython", "skbio"] = "plain", + ids: Optional[List[str]] = None, +) -> Union[List[str], Any]: + """ + Multiple sequence alignment via Kalign. + + Parameters + ---------- + sequences : list of str + List of sequences to align. + seq_type : str or int, optional + Sequence type: "auto", "dna", "rna", "protein" (default: "auto") + gap_open : float, optional + Gap opening penalty. When set, mode is ignored and "fast" preset + is used with the specified penalty. + gap_extend : float, optional + Gap extension penalty. + terminal_gap_extend : float, optional + Terminal gap extension penalty. + n_threads : int, optional + Number of threads (default: global setting). + mode : str, optional + Preset mode: "fast", "default", "accurate" (default: "default"). + fmt : {'plain', 'biopython', 'skbio'}, default 'plain' + Return format. + ids : list of str, optional + Sequence IDs (for Biopython/scikit-bio formats). + + Returns + ------- + list of str | Bio.Align.MultipleSeqAlignment | skbio.TabularMSA + """ + _validate_sequences(sequences) + + seq_type_int = _resolve_seq_type(seq_type) + + # Parameter validation for gap penalties + if gap_open is not None: + if not isinstance(gap_open, (int, float)): + raise ValueError("gap_open must be a number") + if gap_open < 0: + raise ValueError("gap_open must be a positive number (penalty value)") + if gap_extend is not None: + if not isinstance(gap_extend, (int, float)): + raise ValueError("gap_extend must be a number") + if gap_extend < 0: + raise ValueError("gap_extend must be a positive number (penalty value)") + if terminal_gap_extend is not None: + if not isinstance(terminal_gap_extend, (int, float)): + raise ValueError("terminal_gap_extend must be a number") + if terminal_gap_extend < 0: raise ValueError( - f"Invalid seq_type: {seq_type}. Must be one of: {list(seq_type_map.keys())}" + "terminal_gap_extend must be a positive number (penalty value)" ) - seq_type_int = seq_type_map[seq_type_lower] - else: - seq_type_int = seq_type - - # Parameter validation and defaults - # The C core expects positive penalty values (e.g., gpo=5.5, gpe=2.0). - # A value of -1.0 signals "use defaults". - if gap_open is None: - gap_open = -1.0 - elif not isinstance(gap_open, (int, float)): - raise ValueError("gap_open must be a number") - elif gap_open < 0: - raise ValueError("gap_open must be a positive number (penalty value)") - - if gap_extend is None: - gap_extend = -1.0 - elif not isinstance(gap_extend, (int, float)): - raise ValueError("gap_extend must be a number") - elif gap_extend < 0: - raise ValueError("gap_extend must be a positive number (penalty value)") - - if terminal_gap_extend is None: - terminal_gap_extend = -1.0 - elif not isinstance(terminal_gap_extend, (int, float)): - raise ValueError("terminal_gap_extend must be a number") - elif terminal_gap_extend < 0: - raise ValueError( - "terminal_gap_extend must be a positive number (penalty value)" - ) # Handle thread count if n_threads is None: @@ -376,84 +311,52 @@ def align( raise ValueError("n_threads must be an integer") elif n_threads < 1: raise ValueError("n_threads must be at least 1") - elif n_threads > 1024: # reasonable upper limit - import warnings - + elif n_threads > 1024: warnings.warn( - f"Very high thread count ({n_threads}) may not improve performance and could cause issues.", + f"Very high thread count ({n_threads}) may not improve performance.", UserWarning, stacklevel=2, ) - # Resolve mode presets — collect explicitly-set params - _explicit = {} - # We detect "explicit" by checking if the value differs from the function signature default. - # For mode-relevant params, the signature defaults match the "default" mode preset. - if ensemble != 0: - _explicit["ensemble"] = ensemble - if realign != 0: - _explicit["realign"] = realign - if consistency != 5: - _explicit["consistency"] = consistency - if consistency_weight != 2.0: - _explicit["consistency_weight"] = consistency_weight - if vsm_amax != -1.0: - _explicit["vsm_amax"] = vsm_amax - - resolved = _resolve_mode(mode, _explicit) - ensemble = resolved.get("ensemble", ensemble) - realign = resolved.get("realign", realign) - consistency = resolved.get("consistency", consistency) - consistency_weight = resolved.get("consistency_weight", consistency_weight) - vsm_amax = resolved.get("vsm_amax", vsm_amax) - - # Convert refine mode - refine_int = _parse_refine_mode(refine) - - # Validate ensemble - if not isinstance(ensemble, int) or ensemble < 0: - raise ValueError("ensemble must be a non-negative integer") - - # Call the C++ binding for core alignment + # Gap penalty override rule: if any gap penalty is set, use "fast" preset + has_gap_override = ( + gap_open is not None or gap_extend is not None + or terminal_gap_extend is not None + ) + if has_gap_override: + effective_mode = "fast" + else: + effective_mode = _resolve_mode_name(mode) + confidence_data = None - try: - result = _core.align( - sequences, - seq_type_int, - gap_open, - gap_extend, - terminal_gap_extend, - n_threads, - refine_int, - ensemble, - min_support, - float(seq_weights), - consistency, - consistency_weight, - vsm_amax, - realign, - ensemble_seed, - ) - # When ensemble is used, result is (sequences, confidence_dict) - if isinstance(result, tuple): - aligned_seqs = result[0] - confidence_data = result[1] - else: - aligned_seqs = result - except Exception as e: - raise RuntimeError(f"Alignment failed: {str(e)}") + result = _core.align_mode( + sequences, effective_mode, seq_type_int, + gap_open if gap_open is not None else -1.0, + gap_extend if gap_extend is not None else -1.0, + terminal_gap_extend if terminal_gap_extend is not None else -1.0, + n_threads, + ) + if isinstance(result, tuple): + aligned_seqs = result[0] + confidence_data = result[1] + else: + aligned_seqs = result - # Validate IDs if provided (applies to all formats) + return _format_result( + aligned_seqs, sequences, seq_type_int, confidence_data, fmt, ids + ) + + +def _format_result(aligned_seqs, sequences, seq_type_int, confidence_data, fmt, ids): + """Format alignment result based on requested format.""" if ids is not None and len(ids) != len(aligned_seqs): raise ValueError( f"Number of IDs ({len(ids)}) must match number of sequences ({len(aligned_seqs)})" ) - # Handle different return formats if fmt == "plain": return aligned_seqs - # Generate IDs if not provided (only for ecosystem formats) if ids is None: ids = [f"seq{i}" for i in range(len(aligned_seqs))] @@ -470,14 +373,12 @@ def align( records = [] for idx, (s, i) in enumerate(zip(aligned_seqs, ids)): rec = SeqRecord(Seq(s), id=i) - # Attach per-residue confidence as letter_annotations if available if confidence_data is not None: res_conf = confidence_data["residue_confidence"] if idx < len(res_conf) and len(res_conf[idx]) == len(s): pp_str = _confidence_to_pp_string(s, res_conf[idx]) rec.letter_annotations["posterior_probability"] = pp_str records.append(rec) - return MultipleSeqAlignment(records) if fmt == "skbio": @@ -490,7 +391,6 @@ def align( "scikit-bio not installed. Run: pip install kalign-python[skbio]" ) from e - # Select the appropriate skbio sequence type skbio_type_map = { DNA: skbio_seq.DNA, DNA_INTERNAL: skbio_seq.DNA, @@ -501,7 +401,6 @@ def align( if seq_type_int in skbio_type_map: SeqClass = skbio_type_map[seq_type_int] else: - # AUTO or unknown: infer from sequence content SeqClass = _infer_skbio_type(sequences, skbio_seq) return TabularMSA( @@ -511,139 +410,17 @@ def align( raise ValueError(f"Unknown fmt='{fmt}' (expected 'plain', 'biopython', 'skbio')") -def _parse_refine_mode(refine): - """Convert string or int refine mode to integer constant.""" - if isinstance(refine, int): - return refine - refine_map = { - "none": REFINE_NONE, - "all": REFINE_ALL, - "confident": REFINE_CONFIDENT, - "inline": REFINE_INLINE, - } - refine_lower = refine.lower() - if refine_lower not in refine_map: - raise ValueError( - f"Invalid refine mode: {refine}. Must be one of: {list(refine_map.keys())}" - ) - return refine_map[refine_lower] - - -def _infer_skbio_type(sequences, skbio_seq): - """Infer the appropriate skbio sequence class from raw sequence content.""" - chars = set() - for seq in sequences: - chars.update(seq.upper().replace("-", "").replace(".", "")) - dna_chars = set("ACGTNRYSWKMBDHV") - rna_chars = set("ACGUNRYSWKMBDHV") - if "U" in chars and "T" not in chars and chars <= rna_chars: - return skbio_seq.RNA - if chars <= dna_chars: - return skbio_seq.DNA - return skbio_seq.Protein - - -def _resolve_mode(mode, explicit_kwargs): - """Resolve mode presets, letting explicit parameters override. - - Mode presets were derived from NSGA-III multi-objective optimization - (objectives: BAliBASE F1, TC, wall-clock time) with 5-fold cross- - validation on BAliBASE v4 (218 protein families). Each tier represents - a Pareto-optimal configuration: - - - "fast": Single run, optimized gap penalties. ~F1 0.74. - - "default": 3-run ensemble with heterogeneous scoring. ~F1 0.79. - - "accurate": 5-run ensemble with confident refinement. ~F1 0.81. - - Explicit keyword arguments override the preset values. - - Parameters - ---------- - mode : str or None - One of "default", "fast", "accurate", or None (treated as "default"). - "precise" is accepted as a deprecated alias for "accurate". - explicit_kwargs : dict - Only keys that the caller *explicitly* passed (not sentinel/default). - - Returns - ------- - dict - Merged parameter values: mode defaults + explicit overrides. - """ - import warnings - - if mode is None: - mode = "default" - mode_lower = mode.lower() - - if mode_lower == "precise": - warnings.warn( - 'mode="precise" is deprecated, use mode="accurate" instead.', - DeprecationWarning, - stacklevel=3, - ) - mode_lower = "accurate" - - if mode_lower not in _MODE_PRESETS: - raise ValueError( - f"Invalid mode: {mode!r}. Must be one of: 'default', 'fast', 'accurate'" - ) - result = dict(_MODE_PRESETS[mode_lower]) - result.update(explicit_kwargs) - return result - - def set_num_threads(n: int) -> None: - """ - Set the default number of threads for alignment operations. - - This affects all future calls to align() that don't explicitly specify n_threads. - The setting is thread-local, so different threads can have different defaults. - - Parameters - ---------- - n : int - Number of threads to use. Must be at least 1. - - Raises - ------ - ValueError - If n is less than 1 - - Examples - -------- - >>> import kalign - >>> kalign.set_num_threads(4) - >>> aligned = kalign.align(sequences) # Uses 4 threads - """ + """Set the default number of threads for alignment operations.""" global _default_threads if n < 1: raise ValueError("Number of threads must be at least 1") - - # Use thread-local storage for thread safety _thread_local.num_threads = n - # Also update global default for new threads _default_threads = n def get_num_threads() -> int: - """ - Get the current default number of threads for alignment operations. - - Returns - ------- - int - Current default number of threads - - Examples - -------- - >>> import kalign - >>> kalign.get_num_threads() - 1 - >>> kalign.set_num_threads(8) - >>> kalign.get_num_threads() - 8 - """ + """Get the current default number of threads.""" return getattr(_thread_local, "num_threads", _default_threads) @@ -654,19 +431,6 @@ def align_from_file( gap_extend: Optional[float] = None, terminal_gap_extend: Optional[float] = None, n_threads: Optional[int] = None, - refine: Union[str, int] = "none", - adaptive_budget: bool = False, - ensemble: int = 0, - ensemble_seed: int = 42, - dist_scale: float = 0.0, - vsm_amax: float = -1.0, - min_support: int = 0, - realign: int = 0, - save_poar: str = "", - load_poar: str = "", - seq_weights: float = 0.0, - consistency: int = 5, - consistency_weight: float = 2.0, mode: Optional[str] = None, ) -> AlignedSequences: """ @@ -675,138 +439,57 @@ def align_from_file( Parameters ---------- input_file : str - Path to input file containing sequences. Supported formats: - FASTA, MSF, Clustal, aligned FASTA. + Path to input file (FASTA, MSF, Clustal). seq_type : str or int, optional - Sequence type specification (same as align function) - gap_open : float, optional - Gap opening penalty - gap_extend : float, optional - Gap extension penalty - terminal_gap_extend : float, optional - Terminal gap extension penalty + Sequence type (default: "auto"). + gap_open, gap_extend, terminal_gap_extend : float, optional + Gap penalties. When set, mode is ignored and "fast" preset is used. n_threads : int, optional - Number of threads to use for alignment - vsm_amax : float, optional - Variable scoring matrix a_max parameter (default: -1.0 = use - kalign defaults: 2.0 for protein, 0.0 for DNA/RNA). Set to 0.0 - to disable. When > 0, subtracts max(0, amax - d) from all - substitution scores, where d is the estimated pairwise distance. + Number of threads. + mode : str, optional + Preset mode: "fast", "default", "accurate" (default: "default"). Returns ------- AlignedSequences - Named tuple with ``names`` and ``sequences`` fields. - - Raises - ------ - FileNotFoundError - If input file doesn't exist - RuntimeError - If alignment fails """ - if not os.path.exists(input_file): raise FileNotFoundError(f"Input file not found: {input_file}") - # Convert string sequence types to integers - seq_type_map = { - "auto": AUTO, - "dna": DNA, - "rna": RNA, - "protein": PROTEIN, - "pfasum43": PROTEIN_PFASUM43, - "pfasum60": PROTEIN_PFASUM60, - "pfasum": PROTEIN_PFASUM_AUTO, - "divergent": PROTEIN_DIVERGENT, - "internal": DNA_INTERNAL, - } + seq_type_int = _resolve_seq_type(seq_type) - if isinstance(seq_type, str): - seq_type_lower = seq_type.lower() - if seq_type_lower not in seq_type_map: - raise ValueError( - f"Invalid seq_type: {seq_type}. Must be one of: {list(seq_type_map.keys())}" - ) - seq_type_int = seq_type_map[seq_type_lower] - else: - seq_type_int = seq_type - - # Use defaults if not specified - if gap_open is None: - gap_open = -1.0 - if gap_extend is None: - gap_extend = -1.0 - if terminal_gap_extend is None: - terminal_gap_extend = -1.0 - - # Handle thread count if n_threads is None: n_threads = get_num_threads() if n_threads < 1: raise ValueError("n_threads must be at least 1") - # Resolve mode presets - _explicit = {} - if ensemble != 0: - _explicit["ensemble"] = ensemble - if realign != 0: - _explicit["realign"] = realign - if consistency != 5: - _explicit["consistency"] = consistency - if consistency_weight != 2.0: - _explicit["consistency_weight"] = consistency_weight - if vsm_amax != -1.0: - _explicit["vsm_amax"] = vsm_amax - - resolved = _resolve_mode(mode, _explicit) - ensemble = resolved.get("ensemble", ensemble) - realign = resolved.get("realign", realign) - consistency = resolved.get("consistency", consistency) - consistency_weight = resolved.get("consistency_weight", consistency_weight) - vsm_amax = resolved.get("vsm_amax", vsm_amax) - - # Convert refine mode - refine_int = _parse_refine_mode(refine) - - # Call the C++ binding — returns (names, sequences) or (names, sequences, confidence) - try: - result = _core.align_from_file( - input_file, - seq_type_int, - gap_open, - gap_extend, - terminal_gap_extend, - n_threads, - refine_int, - int(adaptive_budget), - ensemble, - ensemble_seed, - dist_scale, - vsm_amax, - min_support, - realign, - save_poar, - load_poar, - float(seq_weights), - consistency, - consistency_weight, + has_gap_override = ( + gap_open is not None or gap_extend is not None + or terminal_gap_extend is not None + ) + if has_gap_override: + effective_mode = "fast" + else: + effective_mode = _resolve_mode_name(mode) + + result = _core.align_from_file_mode( + input_file, effective_mode, seq_type_int, + gap_open if gap_open is not None else -1.0, + gap_extend if gap_extend is not None else -1.0, + terminal_gap_extend if terminal_gap_extend is not None else -1.0, + n_threads, + ) + if len(result) == 3: + names, sequences, conf = result + col_conf = list(conf["column_confidence"]) + res_conf = [list(row) for row in conf["residue_confidence"]] + return AlignedSequences( + names=names, sequences=sequences, + column_confidence=col_conf, residue_confidence=res_conf, ) - if len(result) == 3: - names, sequences, conf = result - col_conf = list(conf["column_confidence"]) - res_conf = [list(row) for row in conf["residue_confidence"]] - return AlignedSequences( - names=names, - sequences=sequences, - column_confidence=col_conf, - residue_confidence=res_conf, - ) - else: - names, sequences = result - return AlignedSequences(names=names, sequences=sequences) - except Exception as e: - raise RuntimeError(f"Alignment failed: {str(e)}") + else: + names, sequences = result + return AlignedSequences(names=names, sequences=sequences) def write_alignment( @@ -830,113 +513,42 @@ def write_alignment( Output format: "fasta", "clustal", "stockholm", "phylip" (default: "fasta") ids : list of str, optional Sequence IDs. If None, generates seq0, seq1, etc. - - Raises - ------ - ValueError - If invalid format or empty sequence list - ImportError - If Biopython is not installed for non-FASTA formats - - Examples - -------- - >>> aligned = kalign.align(sequences) - >>> kalign.write_alignment(aligned, "output.fasta") - >>> kalign.write_alignment(aligned, "output.aln", format="clustal", ids=["seq1", "seq2"]) """ - if not sequences: raise ValueError("Empty sequence list provided") format_lower = format.lower() - - # Map format aliases format_map = { - "fasta": "fasta", - "fa": "fasta", - "clustal": "clustal", - "aln": "clustal", - "stockholm": "stockholm", - "sto": "stockholm", - "phylip": "phylip", - "phy": "phylip", + "fasta": "fasta", "fa": "fasta", + "clustal": "clustal", "aln": "clustal", + "stockholm": "stockholm", "sto": "stockholm", + "phylip": "phylip", "phy": "phylip", } - if format_lower not in format_map: raise ValueError( f"Invalid format: {format}. Must be one of: fasta, clustal, stockholm, phylip" ) - mapped_format = format_map[format_lower] - # Use appropriate writer from io module (lazy import to avoid circular imports) - from . import io - + from . import io as _io if mapped_format == "fasta": - io.write_fasta(sequences, output_file, ids=ids) + _io.write_fasta(sequences, output_file, ids=ids) elif mapped_format == "clustal": - io.write_clustal(sequences, output_file, ids=ids) + _io.write_clustal(sequences, output_file, ids=ids) elif mapped_format == "stockholm": - io.write_stockholm( - sequences, - output_file, - ids=ids, + _io.write_stockholm( + sequences, output_file, ids=ids, column_confidence=column_confidence, residue_confidence=residue_confidence, ) elif mapped_format == "phylip": - io.write_phylip(sequences, output_file, ids=ids) + _io.write_phylip(sequences, output_file, ids=ids) def generate_test_sequences( n_seq: int, n_obs: int, dna: bool, length: int, seed: int = 42 ) -> List[str]: - """ - Generate test sequences using DSSim HMM-based simulator. - - This function uses the DSSim (Dynamic Sequence Simulator) from the Kalign - test suite to generate realistic evolutionary sequence data for testing - and benchmarking purposes. - - Parameters - ---------- - n_seq : int - Number of sequences to generate - n_obs : int - Number of observed sequences for training the HMM (typically 20-50) - dna : bool - True to generate DNA sequences, False to generate protein sequences - length : int - Target sequence length - seed : int, optional - Random seed for reproducible results (default: 42) - - Returns - ------- - list of str - List of generated sequences - - Raises - ------ - RuntimeError - If sequence generation fails - - Examples - -------- - >>> import kalign - >>> # Generate 100 DNA sequences of length 200 - >>> dna_seqs = kalign.generate_test_sequences(100, 20, True, 200) - >>> len(dna_seqs) - 100 - >>> len(dna_seqs[0]) - 200 - - >>> # Generate 50 protein sequences of length 150 - >>> protein_seqs = kalign.generate_test_sequences(50, 30, False, 150) - >>> len(protein_seqs) - 50 - """ - + """Generate test sequences using DSSim HMM-based simulator.""" if n_seq < 1: raise ValueError("n_seq must be at least 1") if n_obs < 1: @@ -944,45 +556,30 @@ def generate_test_sequences( if length < 1: raise ValueError("length must be at least 1") - try: - sequences = _core.generate_test_sequences(n_seq, n_obs, dna, length, seed) - return sequences - except Exception as e: - raise RuntimeError(f"Test sequence generation failed: {str(e)}") + sequences = _core.generate_test_sequences(n_seq, n_obs, dna, length, seed) + return sequences def compare(reference_file: str, test_file: str) -> float: """ Compare two multiple sequence alignments and return SP score. - Computes the sum-of-pairs (SP) score between a reference alignment - and a test alignment. Sequences are matched by name, so both files - must contain the same sequences. - Parameters ---------- reference_file : str - Path to reference alignment file (FASTA, MSF, or Clustal format) + Path to reference alignment file test_file : str Path to test alignment file Returns ------- float - SP score (0-100), where 100 means identical alignments - - Raises - ------ - FileNotFoundError - If either file doesn't exist - RuntimeError - If comparison fails + SP score (0-100) """ if not os.path.exists(reference_file): raise FileNotFoundError(f"Reference file not found: {reference_file}") if not os.path.exists(test_file): raise FileNotFoundError(f"Test file not found: {test_file}") - return _core.compare(reference_file, test_file) @@ -993,38 +590,23 @@ def compare_detailed( column_mask: Optional[List[int]] = None, ) -> dict: """ - Compare two multiple sequence alignments returning detailed POAR scores. - - Computes BAliBASE-compatible recall (SP), precision, F1, and TC scores. - Only "core" columns (gap fraction <= max_gap_frac in reference) are scored - for recall/TC. Gap-gap matches are NOT counted. + Compare two MSAs returning detailed POAR scores (recall, precision, F1, TC). Parameters ---------- reference_file : str - Path to reference alignment file (FASTA, MSF, or Clustal format) + Path to reference alignment file test_file : str Path to test alignment file max_gap_frac : float, optional - Maximum gap fraction for a reference column to be scored (default: 0.2). - Use -1.0 to score all columns regardless of gap content. - Ignored when column_mask is provided. + Max gap fraction for scored columns (default: 0.2). column_mask : list of int, optional - Explicit binary mask (0/1) for each column in the reference alignment. - When provided, only columns with mask=1 are scored (overrides max_gap_frac). - Typically parsed from BAliBASE XML core block annotations. + Explicit binary mask for column scoring. Returns ------- dict Keys: recall, precision, f1, tc, ref_pairs, test_pairs, common_pairs - - Raises - ------ - FileNotFoundError - If either file doesn't exist - RuntimeError - If comparison fails """ if not os.path.exists(reference_file): raise FileNotFoundError(f"Reference file not found: {reference_file}") @@ -1033,7 +615,6 @@ def compare_detailed( if column_mask is not None: return _core.compare_detailed_with_mask(reference_file, test_file, column_mask) - return _core.compare_detailed(reference_file, test_file, max_gap_frac) @@ -1046,172 +627,56 @@ def align_file_to_file( gap_extend: Optional[float] = None, terminal_gap_extend: Optional[float] = None, n_threads: Optional[int] = None, - refine: Union[str, int] = "none", - adaptive_budget: bool = False, - ensemble: int = 0, - ensemble_seed: int = 42, - dist_scale: float = 0.0, - vsm_amax: float = -1.0, - min_support: int = 0, - realign: int = 0, - save_poar: str = "", - load_poar: str = "", - seq_weights: float = 0.0, - consistency: int = 5, - consistency_weight: float = 2.0, mode: Optional[str] = None, ) -> None: """ Align sequences from input file and write result to output file. - Unlike align_from_file(), this preserves all sequence metadata (names, - descriptions), which is required for MSA comparison with compare(). - Parameters ---------- input_file : str - Path to input file containing unaligned sequences + Path to input file output_file : str Path to output alignment file format : str, optional Output format: "fasta", "msf", "clu" (default: "fasta") seq_type : str or int, optional Sequence type (default: "auto") - gap_open : float, optional - Gap opening penalty - gap_extend : float, optional - Gap extension penalty - terminal_gap_extend : float, optional - Terminal gap extension penalty + gap_open, gap_extend, terminal_gap_extend : float, optional + Gap penalties. When set, mode is ignored and "fast" preset is used. n_threads : int, optional - Number of threads (default: uses global setting) - vsm_amax : float, optional - Variable scoring matrix a_max parameter (default: -1.0 = use - kalign defaults: 2.0 for protein, 0.0 for DNA/RNA). Set to 0.0 - to disable. - - Raises - ------ - FileNotFoundError - If input file doesn't exist - RuntimeError - If alignment or writing fails + Number of threads. + mode : str, optional + Preset mode: "fast", "default", "accurate" (default: "default"). """ if not os.path.exists(input_file): raise FileNotFoundError(f"Input file not found: {input_file}") - seq_type_map = { - "auto": AUTO, - "dna": DNA, - "rna": RNA, - "protein": PROTEIN, - "pfasum43": PROTEIN_PFASUM43, - "pfasum60": PROTEIN_PFASUM60, - "pfasum": PROTEIN_PFASUM_AUTO, - "divergent": PROTEIN_DIVERGENT, - "internal": DNA_INTERNAL, - } - - if isinstance(seq_type, str): - seq_type_lower = seq_type.lower() - if seq_type_lower not in seq_type_map: - raise ValueError( - f"Invalid seq_type: {seq_type}. Must be one of: {list(seq_type_map.keys())}" - ) - seq_type_int = seq_type_map[seq_type_lower] - else: - seq_type_int = seq_type + seq_type_int = _resolve_seq_type(seq_type) if n_threads is None: n_threads = get_num_threads() - # Handle "precise" → "accurate" alias - import warnings as _w - effective_mode = mode - if mode is not None and mode.lower() == "precise": - _w.warn( - 'mode="precise" is deprecated, use mode="accurate" instead.', - DeprecationWarning, - stacklevel=2, - ) - effective_mode = "accurate" - - # Fast path: if a known preset mode is used with no parameter overrides, - # delegate entirely to the C-side NSGA-III optimized presets. - # Check BEFORE converting None → -1.0 sentinels. - _has_overrides = ( + has_gap_override = ( gap_open is not None or gap_extend is not None or terminal_gap_extend is not None - or ensemble != 0 or realign != 0 - or vsm_amax != -1.0 or consistency != 5 - or consistency_weight != 2.0 or refine != "none" - or seq_weights != 0.0 ) - if (effective_mode is not None - and effective_mode.lower() in _PRESET_MODES - and not _has_overrides): - _core.align_file_to_file_mode( - input_file, output_file, - effective_mode.lower(), format, n_threads, - ) - return - - if gap_open is None: - gap_open = -1.0 - if gap_extend is None: - gap_extend = -1.0 - if terminal_gap_extend is None: - terminal_gap_extend = -1.0 - - # Legacy path: resolve mode presets and pass individual params - _explicit = {} - if ensemble != 0: - _explicit["ensemble"] = ensemble - if realign != 0: - _explicit["realign"] = realign - if consistency != 5: - _explicit["consistency"] = consistency - if consistency_weight != 2.0: - _explicit["consistency_weight"] = consistency_weight - if vsm_amax != -1.0: - _explicit["vsm_amax"] = vsm_amax - - resolved = _resolve_mode(effective_mode, _explicit) - ensemble = resolved.get("ensemble", ensemble) - realign = resolved.get("realign", realign) - consistency = resolved.get("consistency", consistency) - consistency_weight = resolved.get("consistency_weight", consistency_weight) - vsm_amax = resolved.get("vsm_amax", vsm_amax) - - refine_int = _parse_refine_mode(resolved.get("refine", refine)) - - _core.align_file_to_file( - input_file, - output_file, - format, + if has_gap_override: + effective_mode = "fast" + else: + effective_mode = _resolve_mode_name(mode) + + _core.align_file_to_file_mode( + input_file, output_file, effective_mode, format, n_threads, seq_type_int, gap_open if gap_open is not None else -1.0, gap_extend if gap_extend is not None else -1.0, terminal_gap_extend if terminal_gap_extend is not None else -1.0, - n_threads, - refine_int, - int(adaptive_budget), - ensemble, - ensemble_seed, - dist_scale, - vsm_amax, - min_support, - realign, - save_poar, - load_poar, - float(resolved.get("seq_weights", seq_weights)), - consistency, - consistency_weight, ) # Convenience aliases -kalign = align # For backward compatibility or alternative naming +kalign = align __all__ = [ diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index 79491c6..d78df4a 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -69,251 +69,6 @@ static py::object extract_confidence(struct msa* msa_data, int numseq) { return result; } -// Shared alignment helper — builds kalign_run_config and calls kalign_align_full. -// All alignment paths go through the same function, eliminating routing bugs. -static int run_alignment(struct msa* msa_data, int n_threads, int seq_type, - float gap_open, float gap_extend, float terminal_gap_extend, - int refine, int adaptive_budget, - int ensemble, uint64_t ensemble_seed, - float dist_scale, float vsm_amax, - int min_support, int realign, - const std::string& save_poar, - const std::string& load_poar, - float use_seq_weights = -1.0f, - int consistency_anchors = 0, float consistency_weight = 2.0f) -{ - if (!load_poar.empty()) { - return kalign_consensus_from_poar(msa_data, load_poar.c_str(), - min_support > 0 ? min_support : 2); - } - - struct kalign_run_config base = kalign_run_config_defaults(); - base.type = seq_type; - base.gpo = gap_open; - base.gpe = gap_extend; - base.tgpe = terminal_gap_extend; - base.refine = refine; - base.adaptive_budget = adaptive_budget; - base.dist_scale = dist_scale; - base.vsm_amax = vsm_amax; - base.use_seq_weights = use_seq_weights; - base.consistency_anchors = consistency_anchors; - base.consistency_weight = consistency_weight; - base.realign = realign; - - if (ensemble > 0) { - /* Ensemble path: resolve base gap penalties, then generate diversified runs. - We need resolved gap penalties for the scale factors to work. - Use kalign_ensemble (old path) which handles sentinel resolution internally, - OR resolve here and use kalign_align_full. We use the old kalign_ensemble - to maintain exact backward compatibility. */ - const char* save_path = save_poar.empty() ? nullptr : save_poar.c_str(); - return kalign_ensemble(msa_data, n_threads, seq_type, ensemble, - gap_open, gap_extend, terminal_gap_extend, - ensemble_seed, min_support, save_path, - refine, dist_scale, vsm_amax, realign, - use_seq_weights, - consistency_anchors, consistency_weight); - } - - /* Single-run path: all params go through kalign_align_full */ - return kalign_align_full(msa_data, &base, 1, nullptr, n_threads); -} - -// Main alignment function -py::object align_sequences( - const std::vector& sequences, - int seq_type = KALIGN_TYPE_UNDEFINED, - float gap_open = -1.0f, - float gap_extend = -1.0f, - float terminal_gap_extend = -1.0f, - int n_threads = 1, - int refine = KALIGN_REFINE_NONE, - int ensemble = 0, - int min_support = 0, - float seq_weights = -1.0f, - int consistency_anchors = 0, float consistency_weight = 2.0f, - float vsm_amax = -1.0f, - int realign = 0, - uint64_t ensemble_seed = 42 -) { - if (sequences.empty()) { - throw std::invalid_argument("Empty sequence list provided"); - } - - // Convert Python strings to C format - std::vector seq_ptrs; - std::vector seq_lengths; - seq_ptrs.reserve(sequences.size()); - seq_lengths.reserve(sequences.size()); - - for (const auto& seq : sequences) { - seq_ptrs.push_back(const_cast(seq.c_str())); - seq_lengths.push_back(static_cast(seq.length())); - } - - if (n_threads < 1) { - n_threads = 1; - } - - // Build msa struct from input arrays - struct msa* msa_data = nullptr; - int result = kalign_arr_to_msa(seq_ptrs.data(), seq_lengths.data(), - static_cast(sequences.size()), &msa_data); - if (result != 0 || !msa_data) { - throw std::runtime_error("Failed to create MSA from input sequences"); - } - msa_data->quiet = 1; - - // Route to appropriate alignment function - result = run_alignment(msa_data, n_threads, seq_type, - gap_open, gap_extend, terminal_gap_extend, - refine, 0, - ensemble, ensemble_seed, - 0.0f, vsm_amax, - min_support, realign, - "", "", - seq_weights, - consistency_anchors, consistency_weight); - - if (result != 0) { - kalign_free_msa(msa_data); - throw std::runtime_error("Kalign alignment failed with error code: " + std::to_string(result)); - } - - // Extract confidence data before converting to arrays (which frees the MSA) - py::object confidence = extract_confidence(msa_data, static_cast(sequences.size())); - - // Extract aligned sequences - char** aligned_seqs = nullptr; - int alignment_length = 0; - result = kalign_msa_to_arr(msa_data, &aligned_seqs, &alignment_length); - kalign_free_msa(msa_data); - - if (result != 0 || !aligned_seqs) { - throw std::runtime_error("Failed to extract aligned sequences"); - } - - // Convert results back to Python - auto seqs = c_strings_to_python(aligned_seqs, static_cast(sequences.size()), alignment_length); - - // If ensemble was used and confidence data exists, return tuple - if (ensemble > 0 && !confidence.is_none()) { - return py::make_tuple(seqs, confidence); - } - - // Otherwise return just sequences (backward compat) - return py::cast(seqs); -} - -// File-based alignment function — returns (names, sequences) or (names, sequences, confidence) -py::object align_from_file( - const std::string& input_file, - int seq_type = KALIGN_TYPE_UNDEFINED, - float gap_open = -1.0f, - float gap_extend = -1.0f, - float terminal_gap_extend = -1.0f, - int n_threads = 1, - int refine = KALIGN_REFINE_NONE, - int adaptive_budget = 0, - int ensemble = 0, - uint64_t ensemble_seed = 42, - float dist_scale = 0.0f, - float vsm_amax = -1.0f, - int min_support = 0, - int realign = 0, - const std::string& save_poar = "", - const std::string& load_poar = "", - float seq_weights = -1.0f, - int consistency_anchors = 0, float consistency_weight = 2.0f -) { - struct msa* msa_data = nullptr; - - // Read input file - int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); - if (result != 0) { - throw std::runtime_error("Failed to read input file: " + input_file); - } - - // Check if msa_data is NULL - this happens when the file format cannot be detected - if (!msa_data) { - throw std::runtime_error("Could not detect valid sequence format in file: " + input_file); - } - - // Perform alignment - result = run_alignment(msa_data, n_threads, seq_type, - gap_open, gap_extend, terminal_gap_extend, - refine, adaptive_budget, - ensemble, ensemble_seed, - dist_scale, vsm_amax, - min_support, realign, - save_poar, load_poar, - seq_weights, - consistency_anchors, consistency_weight); - if (result != 0) { - kalign_free_msa(msa_data); - throw std::runtime_error("Kalign alignment failed with error code: " + std::to_string(result)); - } - - // Extract confidence data before writing (which doesn't preserve it) - py::object confidence = extract_confidence(msa_data, msa_data->numseq); - - // Write to a temporary file so kalign_write_msa handles gap insertion - const char* tmpdir = std::getenv("TMPDIR"); - if (!tmpdir) tmpdir = std::getenv("TMP"); - if (!tmpdir) tmpdir = std::getenv("TEMP"); - if (!tmpdir) tmpdir = "/tmp"; - std::string temp_file = std::string(tmpdir) + "/kalign_output.fa"; - result = kalign_write_msa(msa_data, const_cast(temp_file.c_str()), const_cast("fasta")); - - kalign_free_msa(msa_data); - - if (result != 0) { - throw std::runtime_error("Failed to write alignment results"); - } - - // Parse the FASTA file, capturing both headers (names) and sequences - std::ifstream file(temp_file); - std::vector names; - std::vector aligned_sequences; - std::string line, current_name, current_seq; - - while (std::getline(file, line)) { - if (line.empty()) continue; - - if (line[0] == '>') { - if (!current_seq.empty()) { - names.push_back(current_name); - aligned_sequences.push_back(current_seq); - current_seq.clear(); - } - // Strip the '>' prefix; take everything up to the first whitespace as the name - current_name = line.substr(1); - auto ws = current_name.find_first_of(" \t"); - if (ws != std::string::npos) { - current_name = current_name.substr(0, ws); - } - } else { - current_seq += line; - } - } - - if (!current_seq.empty()) { - names.push_back(current_name); - aligned_sequences.push_back(current_seq); - } - - // Clean up temp file - std::remove(temp_file.c_str()); - - // If confidence data exists, return 3-tuple - if (!confidence.is_none()) { - return py::make_tuple(names, aligned_sequences, confidence); - } - - return py::make_tuple(names, aligned_sequences); -} - // Generate test sequences using DSSim std::vector generate_test_sequences( int n_seq, @@ -462,58 +217,6 @@ py::dict compare_detailed_with_mask_files(const std::string& reference_file, return d; } -// Align sequences from input file and write result to output file, preserving all metadata -void align_file_to_file( - const std::string& input_file, - const std::string& output_file, - const std::string& format = "fasta", - int seq_type = KALIGN_TYPE_UNDEFINED, - float gap_open = -1.0f, - float gap_extend = -1.0f, - float terminal_gap_extend = -1.0f, - int n_threads = 1, - int refine = KALIGN_REFINE_NONE, - int adaptive_budget = 0, - int ensemble = 0, - uint64_t ensemble_seed = 42, - float dist_scale = 0.0f, - float vsm_amax = -1.0f, - int min_support = 0, - int realign = 0, - const std::string& save_poar = "", - const std::string& load_poar = "", - float seq_weights = -1.0f, - int consistency_anchors = 0, float consistency_weight = 2.0f -) { - struct msa* msa_data = nullptr; - - int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); - if (result != 0 || !msa_data) { - throw std::runtime_error("Failed to read input file: " + input_file); - } - - result = run_alignment(msa_data, n_threads, seq_type, - gap_open, gap_extend, terminal_gap_extend, - refine, adaptive_budget, - ensemble, ensemble_seed, - dist_scale, vsm_amax, - min_support, realign, - save_poar, load_poar, - seq_weights, - consistency_anchors, consistency_weight); - if (result != 0) { - kalign_free_msa(msa_data); - throw std::runtime_error("Alignment failed with error code: " + std::to_string(result)); - } - - result = kalign_write_msa(msa_data, const_cast(output_file.c_str()), const_cast(format.c_str())); - kalign_free_msa(msa_data); - - if (result != 0) { - throw std::runtime_error("Failed to write output file: " + output_file); - } -} - // Ensemble with per-run parameters — playground for optimization. // Each run gets its own gap penalties, matrix type, and tree noise. // Now uses kalign_align_full with per-run configs. @@ -535,7 +238,15 @@ void ensemble_custom_file_to_file( float seq_weights = -1.0f, int n_threads = 1, int consistency_anchors = 0, - float consistency_weight = 2.0f + float consistency_weight = 2.0f, + // Per-run overrides: when non-empty, override the shared value per-run. + // Same pattern as run_types: empty = use shared value for all runs. + const std::vector& run_vsm_amax = {}, + const std::vector& run_seq_weights = {}, + const std::vector& run_refine = {}, + const std::vector& run_realign = {}, + const std::vector& run_consistency_anchors = {}, + const std::vector& run_consistency_weight = {} ) { int n_runs = static_cast(run_gpo.size()); if (n_runs < 1) { @@ -546,9 +257,28 @@ void ensemble_custom_file_to_file( static_cast(run_noise.size()) != n_runs) { throw std::invalid_argument("All per-run arrays must have the same length"); } + // Validate optional per-run arrays: must be empty or same length as run_gpo if (!run_types.empty() && static_cast(run_types.size()) != n_runs) { throw std::invalid_argument("run_types must be empty or same length as run_gpo"); } + if (!run_vsm_amax.empty() && static_cast(run_vsm_amax.size()) != n_runs) { + throw std::invalid_argument("run_vsm_amax must be empty or same length as run_gpo"); + } + if (!run_seq_weights.empty() && static_cast(run_seq_weights.size()) != n_runs) { + throw std::invalid_argument("run_seq_weights must be empty or same length as run_gpo"); + } + if (!run_refine.empty() && static_cast(run_refine.size()) != n_runs) { + throw std::invalid_argument("run_refine must be empty or same length as run_gpo"); + } + if (!run_realign.empty() && static_cast(run_realign.size()) != n_runs) { + throw std::invalid_argument("run_realign must be empty or same length as run_gpo"); + } + if (!run_consistency_anchors.empty() && static_cast(run_consistency_anchors.size()) != n_runs) { + throw std::invalid_argument("run_consistency_anchors must be empty or same length as run_gpo"); + } + if (!run_consistency_weight.empty() && static_cast(run_consistency_weight.size()) != n_runs) { + throw std::invalid_argument("run_consistency_weight must be empty or same length as run_gpo"); + } struct msa* msa_data = nullptr; int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); @@ -556,27 +286,27 @@ void ensemble_custom_file_to_file( throw std::runtime_error("Failed to read input file: " + input_file); } - /* Build per-run configs */ + /* Build per-run configs. Each optional per-run array overrides the + shared scalar when non-empty, following the run_types pattern. */ std::vector runs(n_runs); for (int k = 0; k < n_runs; k++) { runs[k] = kalign_run_config_defaults(); - runs[k].type = (!run_types.empty()) ? run_types[k] : seq_type; + runs[k].matrix = (!run_types.empty()) ? run_types[k] : seq_type; runs[k].gpo = run_gpo[k]; runs[k].gpe = run_gpe[k]; runs[k].tgpe = run_tgpe[k]; runs[k].tree_seed = seed + static_cast(k); runs[k].tree_noise = run_noise[k]; - runs[k].vsm_amax = vsm_amax; + runs[k].vsm_amax = (!run_vsm_amax.empty()) ? run_vsm_amax[k] : vsm_amax; runs[k].dist_scale = 0.0f; - runs[k].use_seq_weights = seq_weights; - runs[k].refine = refine; - runs[k].realign = realign; - runs[k].consistency_anchors = consistency_anchors; - runs[k].consistency_weight = consistency_weight; + runs[k].seq_weights = (!run_seq_weights.empty()) ? run_seq_weights[k] : seq_weights; + runs[k].refine = (!run_refine.empty()) ? run_refine[k] : refine; + runs[k].realign = (!run_realign.empty()) ? run_realign[k] : realign; + runs[k].consistency_anchors = (!run_consistency_anchors.empty()) ? run_consistency_anchors[k] : consistency_anchors; + runs[k].consistency_weight = (!run_consistency_weight.empty()) ? run_consistency_weight[k] : consistency_weight; } struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); - ens.seed = seed; ens.min_support = min_support; result = kalign_align_full(msa_data, runs.data(), n_runs, &ens, n_threads); @@ -594,14 +324,219 @@ void ensemble_custom_file_to_file( } } -// Align using a named mode preset (fast/default/accurate). -// The C library provides NSGA-III optimized protein presets. +// Align in-memory sequences using a named mode preset. +// Detects biotype from sequences and delegates to C preset system. +py::object align_mode( + const std::vector& sequences, + const std::string& mode, + int seq_type = KALIGN_TYPE_UNDEFINED, + float gap_open = -1.0f, + float gap_extend = -1.0f, + float terminal_gap_extend = -1.0f, + int n_threads = 1 +) { + if (sequences.empty()) { + throw std::invalid_argument("Empty sequence list provided"); + } + + std::vector seq_ptrs; + std::vector seq_lengths; + seq_ptrs.reserve(sequences.size()); + seq_lengths.reserve(sequences.size()); + for (const auto& seq : sequences) { + seq_ptrs.push_back(const_cast(seq.c_str())); + seq_lengths.push_back(static_cast(seq.length())); + } + if (n_threads < 1) n_threads = 1; + + struct msa* msa_data = nullptr; + int result = kalign_arr_to_msa(seq_ptrs.data(), seq_lengths.data(), + static_cast(sequences.size()), &msa_data); + if (result != 0 || !msa_data) { + throw std::runtime_error("Failed to create MSA from input sequences"); + } + msa_data->quiet = 1; + + /* Force biotype if caller specified one */ + if (seq_type == KALIGN_MATRIX_DNA || seq_type == KALIGN_MATRIX_DNA_INTERNAL) { + msa_data->biotype = ALN_BIOTYPE_DNA; + } else if (seq_type == KALIGN_MATRIX_RNA) { + msa_data->biotype = ALN_BIOTYPE_DNA; /* RNA uses DNA biotype internally */ + } else if (seq_type != KALIGN_MATRIX_AUTO && seq_type != KALIGN_TYPE_UNDEFINED) { + msa_data->biotype = ALN_BIOTYPE_PROTEIN; + } + + /* Detect biotype if not already set */ + if (msa_data->biotype == ALN_BIOTYPE_UNDEF) { + result = detect_alphabet(msa_data); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Failed to detect sequence type"); + } + } + + /* Get preset configs */ + struct kalign_run_config runs[KALIGN_MAX_PRESET_RUNS]; + struct kalign_ensemble_config ens; + int n_runs = 0; + + result = kalign_get_mode_preset(mode.c_str(), msa_data->biotype, + runs, &n_runs, &ens); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::invalid_argument("Unknown mode: " + mode); + } + + /* Apply user gap penalty overrides to all runs */ + for (int k = 0; k < n_runs; k++) { + if (gap_open >= 0.0f) runs[k].gpo = gap_open; + if (gap_extend >= 0.0f) runs[k].gpe = gap_extend; + if (terminal_gap_extend >= 0.0f) runs[k].tgpe = terminal_gap_extend; + } + + result = kalign_align_full(msa_data, runs, n_runs, + n_runs > 1 ? &ens : nullptr, n_threads); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Alignment failed with error code: " + std::to_string(result)); + } + + py::object confidence = extract_confidence(msa_data, static_cast(sequences.size())); + + char** aligned_seqs = nullptr; + int alignment_length = 0; + result = kalign_msa_to_arr(msa_data, &aligned_seqs, &alignment_length); + kalign_free_msa(msa_data); + + if (result != 0 || !aligned_seqs) { + throw std::runtime_error("Failed to extract aligned sequences"); + } + + auto seqs = c_strings_to_python(aligned_seqs, static_cast(sequences.size()), alignment_length); + + if (n_runs > 1 && !confidence.is_none()) { + return py::make_tuple(seqs, confidence); + } + return py::cast(seqs); +} + +// Align from file using a named mode preset, returning (names, sequences). +py::object align_from_file_mode( + const std::string& input_file, + const std::string& mode, + int seq_type = KALIGN_TYPE_UNDEFINED, + float gap_open = -1.0f, + float gap_extend = -1.0f, + float terminal_gap_extend = -1.0f, + int n_threads = 1 +) { + struct msa* msa_data = nullptr; + int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); + if (result != 0 || !msa_data) { + throw std::runtime_error("Failed to read input file: " + input_file); + } + + /* Force biotype if caller specified one */ + if (seq_type == KALIGN_MATRIX_DNA || seq_type == KALIGN_MATRIX_DNA_INTERNAL) { + msa_data->biotype = ALN_BIOTYPE_DNA; + } else if (seq_type == KALIGN_MATRIX_RNA) { + msa_data->biotype = ALN_BIOTYPE_DNA; + } else if (seq_type != KALIGN_MATRIX_AUTO && seq_type != KALIGN_TYPE_UNDEFINED) { + msa_data->biotype = ALN_BIOTYPE_PROTEIN; + } + + if (msa_data->biotype == ALN_BIOTYPE_UNDEF) { + result = detect_alphabet(msa_data); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Failed to detect sequence type"); + } + } + + struct kalign_run_config runs[KALIGN_MAX_PRESET_RUNS]; + struct kalign_ensemble_config ens; + int n_runs = 0; + + result = kalign_get_mode_preset(mode.c_str(), msa_data->biotype, + runs, &n_runs, &ens); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::invalid_argument("Unknown mode: " + mode); + } + + for (int k = 0; k < n_runs; k++) { + if (gap_open >= 0.0f) runs[k].gpo = gap_open; + if (gap_extend >= 0.0f) runs[k].gpe = gap_extend; + if (terminal_gap_extend >= 0.0f) runs[k].tgpe = terminal_gap_extend; + } + + result = kalign_align_full(msa_data, runs, n_runs, + n_runs > 1 ? &ens : nullptr, n_threads); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Alignment failed with error code: " + std::to_string(result)); + } + + py::object confidence = extract_confidence(msa_data, msa_data->numseq); + + /* Write to temp file to get gap-inserted FASTA output */ + const char* tmpdir = std::getenv("TMPDIR"); + if (!tmpdir) tmpdir = std::getenv("TMP"); + if (!tmpdir) tmpdir = std::getenv("TEMP"); + if (!tmpdir) tmpdir = "/tmp"; + std::string temp_file = std::string(tmpdir) + "/kalign_output.fa"; + result = kalign_write_msa(msa_data, const_cast(temp_file.c_str()), + const_cast("fasta")); + kalign_free_msa(msa_data); + + if (result != 0) { + throw std::runtime_error("Failed to write alignment results"); + } + + std::ifstream file(temp_file); + std::vector names; + std::vector aligned_sequences; + std::string line, current_name, current_seq; + + while (std::getline(file, line)) { + if (line.empty()) continue; + if (line[0] == '>') { + if (!current_seq.empty()) { + names.push_back(current_name); + aligned_sequences.push_back(current_seq); + current_seq.clear(); + } + current_name = line.substr(1); + auto ws = current_name.find_first_of(" \t"); + if (ws != std::string::npos) current_name = current_name.substr(0, ws); + } else { + current_seq += line; + } + } + if (!current_seq.empty()) { + names.push_back(current_name); + aligned_sequences.push_back(current_seq); + } + std::remove(temp_file.c_str()); + + if (!confidence.is_none()) { + return py::make_tuple(names, aligned_sequences, confidence); + } + return py::make_tuple(names, aligned_sequences); +} + +// Align file-to-file using a named mode preset (fast/default/accurate). +// The C library provides NSGA-III optimized presets per biotype. void align_file_to_file_mode( const std::string& input_file, const std::string& output_file, const std::string& mode, const std::string& format = "fasta", - int n_threads = 1 + int n_threads = 1, + int seq_type = KALIGN_TYPE_UNDEFINED, + float gap_open = -1.0f, + float gap_extend = -1.0f, + float terminal_gap_extend = -1.0f ) { struct msa* msa_data = nullptr; int result = kalign_read_input(const_cast(input_file.c_str()), &msa_data, 1); @@ -609,16 +544,42 @@ void align_file_to_file_mode( throw std::runtime_error("Failed to read input file: " + input_file); } + /* Force biotype if caller specified one */ + if (seq_type == KALIGN_MATRIX_DNA || seq_type == KALIGN_MATRIX_DNA_INTERNAL) { + msa_data->biotype = ALN_BIOTYPE_DNA; + } else if (seq_type == KALIGN_MATRIX_RNA) { + msa_data->biotype = ALN_BIOTYPE_DNA; + } else if (seq_type != KALIGN_MATRIX_AUTO && seq_type != KALIGN_TYPE_UNDEFINED) { + msa_data->biotype = ALN_BIOTYPE_PROTEIN; + } + + /* Detect biotype from sequences so we can select the right preset grid slot */ + if (msa_data->biotype == ALN_BIOTYPE_UNDEF) { + result = detect_alphabet(msa_data); + if (result != 0) { + kalign_free_msa(msa_data); + throw std::runtime_error("Failed to detect sequence type"); + } + } + struct kalign_run_config runs[KALIGN_MAX_PRESET_RUNS]; struct kalign_ensemble_config ens; int n_runs = 0; - result = kalign_get_mode_preset(mode.c_str(), runs, &n_runs, &ens); + result = kalign_get_mode_preset(mode.c_str(), msa_data->biotype, + runs, &n_runs, &ens); if (result != 0) { kalign_free_msa(msa_data); throw std::invalid_argument("Unknown mode: " + mode); } + /* Apply user gap penalty overrides to all runs */ + for (int k = 0; k < n_runs; k++) { + if (gap_open >= 0.0f) runs[k].gpo = gap_open; + if (gap_extend >= 0.0f) runs[k].gpe = gap_extend; + if (terminal_gap_extend >= 0.0f) runs[k].tgpe = terminal_gap_extend; + } + result = kalign_align_full(msa_data, runs, n_runs, n_runs > 1 ? &ens : nullptr, n_threads); if (result != 0) { @@ -638,83 +599,6 @@ void align_file_to_file_mode( PYBIND11_MODULE(_core, m) { m.doc() = "Python bindings for Kalign multiple sequence alignment"; - // Main alignment function - m.def("align", &align_sequences, - py::arg("sequences"), - py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, - py::arg("gap_open") = -1.0f, - py::arg("gap_extend") = -1.0f, - py::arg("terminal_gap_extend") = -1.0f, - py::arg("n_threads") = 1, - py::arg("refine") = KALIGN_REFINE_NONE, - py::arg("ensemble") = 0, - py::arg("min_support") = 0, - py::arg("seq_weights") = -1.0f, - py::arg("consistency_anchors") = 0, - py::arg("consistency_weight") = 2.0f, - py::arg("vsm_amax") = -1.0f, - py::arg("realign") = 0, - py::arg("ensemble_seed") = (uint64_t)42, - R"pbdoc( - Align a list of sequences using Kalign. - - Parameters - ---------- - sequences : list of str - List of sequences to align - seq_type : int, optional - Sequence type (default: auto-detect) - gap_open : float, optional - Gap opening penalty (default: -1.0, uses Kalign defaults) - gap_extend : float, optional - Gap extension penalty (default: -1.0, uses Kalign defaults) - terminal_gap_extend : float, optional - Terminal gap extension penalty (default: -1.0, uses Kalign defaults) - n_threads : int, optional - Number of threads to use (default: 1) - refine : int, optional - Refinement mode (default: REFINE_NONE) - ensemble : int, optional - Number of ensemble runs (default: 0 = off) - min_support : int, optional - Explicit consensus threshold (default: 0 = auto) - vsm_amax : float, optional - Variable scoring matrix amplitude (default: -1.0, uses Kalign defaults) - realign : int, optional - Number of realignment iterations (default: 0 = off) - ensemble_seed : int, optional - RNG seed for ensemble (default: 42) - - Returns - ------- - list of str or tuple - When ensemble > 0: (aligned_seqs, confidence_dict) - Otherwise: aligned sequences - )pbdoc"); - - // File-based alignment — returns (names, sequences) or (names, sequences, confidence) - m.def("align_from_file", &align_from_file, - py::arg("input_file"), - py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, - py::arg("gap_open") = -1.0f, - py::arg("gap_extend") = -1.0f, - py::arg("terminal_gap_extend") = -1.0f, - py::arg("n_threads") = 1, - py::arg("refine") = KALIGN_REFINE_NONE, - py::arg("adaptive_budget") = 0, - py::arg("ensemble") = 0, - py::arg("ensemble_seed") = (uint64_t)42, - py::arg("dist_scale") = 0.0f, - py::arg("vsm_amax") = -1.0f, - py::arg("min_support") = 0, - py::arg("realign") = 0, - py::arg("save_poar") = "", - py::arg("load_poar") = "", - py::arg("seq_weights") = -1.0f, - py::arg("consistency_anchors") = 0, - py::arg("consistency_weight") = 2.0f, - "Align sequences from a file. Returns (names, sequences) or (names, sequences, confidence) tuple."); - // Generate test sequences using DSSim m.def("generate_test_sequences", &generate_test_sequences, py::arg("n_seq"), @@ -812,55 +696,6 @@ PYBIND11_MODULE(_core, m) { Keys: recall, precision, f1, tc, ref_pairs, test_pairs, common_pairs )pbdoc"); - // Align file to file (preserves sequence names/metadata) - m.def("align_file_to_file", &align_file_to_file, - py::arg("input_file"), - py::arg("output_file"), - py::arg("format") = "fasta", - py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, - py::arg("gap_open") = -1.0f, - py::arg("gap_extend") = -1.0f, - py::arg("terminal_gap_extend") = -1.0f, - py::arg("n_threads") = 1, - py::arg("refine") = KALIGN_REFINE_NONE, - py::arg("adaptive_budget") = 0, - py::arg("ensemble") = 0, - py::arg("ensemble_seed") = (uint64_t)42, - py::arg("dist_scale") = 0.0f, - py::arg("vsm_amax") = -1.0f, - py::arg("min_support") = 0, - py::arg("realign") = 0, - py::arg("save_poar") = "", - py::arg("load_poar") = "", - py::arg("seq_weights") = -1.0f, - py::arg("consistency_anchors") = 0, - py::arg("consistency_weight") = 2.0f, - R"pbdoc( - Align sequences from input file and write to output file. - - Unlike align_from_file, this preserves all sequence metadata - (names, descriptions) which is required for MSA comparison. - - Parameters - ---------- - input_file : str - Path to input sequence file - output_file : str - Path to output alignment file - format : str, optional - Output format: "fasta", "msf", "clu" (default: "fasta") - seq_type : int, optional - Sequence type (default: auto-detect) - gap_open : float, optional - Gap opening penalty - gap_extend : float, optional - Gap extension penalty - terminal_gap_extend : float, optional - Terminal gap extension penalty - n_threads : int, optional - Number of threads (default: 1) - )pbdoc"); - // Ensemble with per-run parameters (optimization playground) m.def("ensemble_custom_file_to_file", &ensemble_custom_file_to_file, py::arg("input_file"), @@ -881,11 +716,19 @@ PYBIND11_MODULE(_core, m) { py::arg("n_threads") = 1, py::arg("consistency_anchors") = 0, py::arg("consistency_weight") = 2.0f, + py::arg("run_vsm_amax") = std::vector{}, + py::arg("run_seq_weights") = std::vector{}, + py::arg("run_refine") = std::vector{}, + py::arg("run_realign") = std::vector{}, + py::arg("run_consistency_anchors") = std::vector{}, + py::arg("run_consistency_weight") = std::vector{}, R"pbdoc( Ensemble alignment with per-run parameters. Each run gets its own gap penalties, matrix type, and tree noise. - This is a playground for optimizing ensemble configurations. + Additional parameters can optionally be varied per-run by passing + arrays of the same length as run_gpo. When empty (default), the + shared scalar value is used for all runs. Parameters ---------- @@ -902,38 +745,95 @@ PYBIND11_MODULE(_core, m) { run_noise : list of float Per-run tree noise sigma values run_types : list of int, optional - Per-run matrix types (e.g. PROTEIN_PFASUM43, PROTEIN_PFASUM60, PROTEIN). - Empty = use seq_type for all runs. + Per-run matrix types. Empty = use seq_type for all runs. + run_vsm_amax : list of float, optional + Per-run VSM amplitude. Empty = use vsm_amax for all runs. + run_seq_weights : list of float, optional + Per-run profile rebalancing weight. Empty = use seq_weights for all. + run_refine : list of int, optional + Per-run refinement mode (REFINE_* constants). Empty = use refine for all. + run_realign : list of int, optional + Per-run realign iterations. Empty = use realign for all. + run_consistency_anchors : list of int, optional + Per-run consistency rounds. Empty = use consistency_anchors for all. + run_consistency_weight : list of float, optional + Per-run consistency weight. Empty = use consistency_weight for all. )pbdoc"); - // Mode-based alignment (NSGA-III optimized presets) + // In-memory alignment using a named mode preset + m.def("align", &align_mode, + py::arg("sequences"), + py::arg("mode"), + py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, + py::arg("gap_open") = -1.0f, + py::arg("gap_extend") = -1.0f, + py::arg("terminal_gap_extend") = -1.0f, + py::arg("n_threads") = 1, + "Align sequences using a named mode preset (fast/default/accurate)."); + m.def("align_mode", &align_mode, + py::arg("sequences"), + py::arg("mode"), + py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, + py::arg("gap_open") = -1.0f, + py::arg("gap_extend") = -1.0f, + py::arg("terminal_gap_extend") = -1.0f, + py::arg("n_threads") = 1, + "Alias for align(). Align sequences using a named mode preset."); + + // File alignment returning (names, sequences) using a named mode preset + m.def("align_from_file", &align_from_file_mode, + py::arg("input_file"), + py::arg("mode"), + py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, + py::arg("gap_open") = -1.0f, + py::arg("gap_extend") = -1.0f, + py::arg("terminal_gap_extend") = -1.0f, + py::arg("n_threads") = 1, + "Align from file using a named mode preset. Returns (names, sequences) tuple."); + m.def("align_from_file_mode", &align_from_file_mode, + py::arg("input_file"), + py::arg("mode"), + py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, + py::arg("gap_open") = -1.0f, + py::arg("gap_extend") = -1.0f, + py::arg("terminal_gap_extend") = -1.0f, + py::arg("n_threads") = 1, + "Alias for align_from_file(). Align from file using a named mode preset."); + + // File-to-file alignment using a named mode preset + m.def("align_file_to_file", &align_file_to_file_mode, + py::arg("input_file"), + py::arg("output_file"), + py::arg("mode"), + py::arg("format") = "fasta", + py::arg("n_threads") = 1, + py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, + py::arg("gap_open") = -1.0f, + py::arg("gap_extend") = -1.0f, + py::arg("terminal_gap_extend") = -1.0f, + "Align file to file using a named mode preset (fast/default/accurate)."); m.def("align_file_to_file_mode", &align_file_to_file_mode, py::arg("input_file"), py::arg("output_file"), py::arg("mode"), py::arg("format") = "fasta", py::arg("n_threads") = 1, - R"pbdoc( - Align sequences using a named mode preset. - - Uses NSGA-III optimized protein presets with per-run - heterogeneous gap penalties and scoring matrices. - - Parameters - ---------- - input_file : str - Path to input sequence file - output_file : str - Path to output alignment file - mode : str - One of "fast", "default", "accurate" - format : str, optional - Output format (default: "fasta") - n_threads : int, optional - Number of threads (default: 1) - )pbdoc"); - - // Constants for sequence types + py::arg("seq_type") = KALIGN_TYPE_UNDEFINED, + py::arg("gap_open") = -1.0f, + py::arg("gap_extend") = -1.0f, + py::arg("terminal_gap_extend") = -1.0f, + "Alias for align_file_to_file(). Align file to file using a named mode preset."); + + // Matrix constants (canonical names) + m.attr("MATRIX_AUTO") = KALIGN_MATRIX_AUTO; + m.attr("MATRIX_PFASUM43") = KALIGN_MATRIX_PFASUM43; + m.attr("MATRIX_PFASUM60") = KALIGN_MATRIX_PFASUM60; + m.attr("MATRIX_CORBLOSUM66") = KALIGN_MATRIX_CORBLOSUM66; + m.attr("MATRIX_DNA") = KALIGN_MATRIX_DNA; + m.attr("MATRIX_DNA_INTERNAL") = KALIGN_MATRIX_DNA_INTERNAL; + m.attr("MATRIX_RNA") = KALIGN_MATRIX_RNA; + + // Backward compat: old names point to new values m.attr("DNA") = KALIGN_TYPE_DNA; m.attr("DNA_INTERNAL") = KALIGN_TYPE_DNA_INTERNAL; m.attr("RNA") = KALIGN_TYPE_RNA; diff --git a/python-kalign/cli.py b/python-kalign/cli.py index bb7a848..c2fca04 100644 --- a/python-kalign/cli.py +++ b/python-kalign/cli.py @@ -19,7 +19,6 @@ def _resolve_version() -> str: - # kalign-test is the name of the test distribution, which may be installed in test environments. If it's present, use its version; otherwise, fall back to the main kalign distribution. If neither is found, try to import __version__ from the package, and if that fails, return "unknown". for dist_name in ("kalign-python",): try: return dist_version(dist_name) @@ -63,23 +62,29 @@ def _build_parser() -> argparse.ArgumentParser: dest="seq_type", help="Sequence type: auto, dna, rna, internal, protein, divergent (default: auto).", ) + parser.add_argument( + "--mode", + default="default", + choices=["fast", "default", "accurate"], + help="Alignment mode preset (default: default).", + ) parser.add_argument( "--gpo", type=float, default=None, - help="Gap open penalty (default: Kalign internal defaults).", + help="Gap open penalty (overrides mode preset).", ) parser.add_argument( "--gpe", type=float, default=None, - help="Gap extension penalty (default: Kalign internal defaults).", + help="Gap extension penalty (overrides mode preset).", ) parser.add_argument( "--tgpe", type=float, default=None, - help="Terminal gap extension penalty (default: Kalign internal defaults).", + help="Terminal gap extension penalty (overrides mode preset).", ) parser.add_argument( "-n", @@ -88,69 +93,6 @@ def _build_parser() -> argparse.ArgumentParser: default=1, help="Number of threads to use (default: 1).", ) - parser.add_argument( - "--refine", - default="confident", - help="Refinement mode: none, all, confident (default: confident).", - ) - parser.add_argument( - "--adaptive-budget", - action="store_true", - default=False, - help="Scale refinement trial count by uncertainty.", - ) - ens = parser.add_argument_group( - "ensemble options", - "These options only take effect when --ensemble is used.", - ) - ens.add_argument( - "--ensemble", - type=int, - default=0, - help="Number of ensemble runs (default: 0 = off). Try 3-5 for better accuracy.", - ) - ens.add_argument( - "--ensemble-seed", - type=int, - default=42, - help="RNG seed for ensemble (default: 42).", - ) - ens.add_argument( - "--min-support", - type=int, - default=0, - help="Explicit consensus threshold (default: 0 = auto).", - ) - ens.add_argument( - "--save-poar", - default=None, - help="Save POAR consensus table to file for later re-thresholding.", - ) - ens.add_argument( - "--load-poar", - default=None, - help="Load POAR consensus table from file (skip alignment, just re-threshold).", - ) - - adv = parser.add_argument_group("advanced options") - adv.add_argument( - "--dist-scale", - type=float, - default=0.0, - help="Distance scaling parameter (default: 0.0).", - ) - adv.add_argument( - "--vsm-amax", - type=float, - default=None, - help="Variable Scoring Matrix a_max (default: Kalign internal defaults).", - ) - adv.add_argument( - "--realign", - type=int, - default=0, - help="Realignment iterations (default: 0 = off).", - ) parser.add_argument( "-V", @@ -204,30 +146,16 @@ def main(argv: Optional[list[str]] = None) -> int: input_path = str(tmp_path) try: - kwargs = dict( + result = kalign.align_from_file( + input_path, seq_type=args.seq_type, gap_open=args.gpo, gap_extend=args.gpe, terminal_gap_extend=args.tgpe, n_threads=args.nthreads, - refine=args.refine, - adaptive_budget=args.adaptive_budget, - ensemble=args.ensemble, - ensemble_seed=args.ensemble_seed, - min_support=args.min_support, - realign=args.realign, - dist_scale=args.dist_scale, + mode=args.mode, ) - if args.vsm_amax is not None: - kwargs["vsm_amax"] = args.vsm_amax - if args.save_poar is not None: - kwargs["save_poar"] = args.save_poar - if args.load_poar is not None: - kwargs["load_poar"] = args.load_poar - - result = kalign.align_from_file(input_path, **kwargs) - # Pass confidence data to Stockholm writer when available write_kwargs = dict( format=args.format, ids=result.names, diff --git a/src/run_kalign.c b/src/run_kalign.c index 81b9d83..f835e5c 100644 --- a/src/run_kalign.c +++ b/src/run_kalign.c @@ -443,16 +443,14 @@ int run_kalign(struct parameters* param) }else{ /* Single-run: use kalign_align_full */ struct kalign_run_config run = kalign_run_config_defaults(); - run.type = param->type; + run.matrix = param->type; run.gpo = param->gpo; run.gpe = param->gpe; run.tgpe = param->tgpe; run.refine = param->refine; run.adaptive_budget = param->adaptive_budget; run.vsm_amax = param->vsm_amax; - run.use_seq_weights = -1.0f; - run.consistency_anchors = param->consistency_anchors; - run.consistency_weight = param->consistency_weight; + run.seq_weights = -1.0f; run.realign = param->realign; RUN(kalign_align_full(msa, &run, 1, NULL, param->nthreads)); } diff --git a/tests/python/test_ecosystem_integration.py b/tests/python/test_ecosystem_integration.py index 0c32b64..11570f2 100644 --- a/tests/python/test_ecosystem_integration.py +++ b/tests/python/test_ecosystem_integration.py @@ -318,6 +318,8 @@ def test_module_exports(self): "MODE_DEFAULT", "MODE_FAST", "MODE_PRECISE", + "MODE_ACCURATE", + "PROTEIN_CORBLOSUM66", "__version__", "__author__", "__email__", diff --git a/tests/python/test_modes.py b/tests/python/test_modes.py index 63d1645..c0aef33 100644 --- a/tests/python/test_modes.py +++ b/tests/python/test_modes.py @@ -24,7 +24,8 @@ class TestModeConstants: def test_mode_constants_exist(self): assert kalign.MODE_DEFAULT == "default" assert kalign.MODE_FAST == "fast" - assert kalign.MODE_PRECISE == "precise" + assert kalign.MODE_ACCURATE == "accurate" + assert kalign.MODE_PRECISE == "precise" # deprecated alias class TestAlignModes: @@ -51,9 +52,9 @@ def test_fast_mode(self): assert all(len(s) == len(result[0]) for s in result) def test_precise_mode(self): - """mode='precise' produces alignment (ensemble).""" - result = kalign.align(TEST_SEQUENCES, mode="precise") - # precise uses ensemble, which returns (seqs, confidence) tuple + """mode='precise' is deprecated alias for 'accurate'.""" + with pytest.warns(DeprecationWarning, match="precise"): + result = kalign.align(TEST_SEQUENCES, mode="precise") if isinstance(result, tuple): seqs = result[0] else: @@ -61,18 +62,20 @@ def test_precise_mode(self): assert len(seqs) == len(TEST_SEQUENCES) assert all(len(s) == len(seqs[0]) for s in seqs) + def test_accurate_mode(self): + """mode='accurate' produces alignment.""" + result = kalign.align(TEST_SEQUENCES, mode="accurate") + if isinstance(result, tuple): + seqs = result[0] + else: + seqs = result + assert len(seqs) == len(TEST_SEQUENCES) + def test_invalid_mode(self): """Invalid mode raises ValueError.""" with pytest.raises(ValueError, match="Invalid mode"): kalign.align(TEST_SEQUENCES, mode="turbo") - def test_explicit_param_overrides_mode(self): - """Explicit consistency=10 overrides fast mode default (consistency=0).""" - # This should not crash — fast base + explicit consistency - result = kalign.align(TEST_SEQUENCES, mode="fast", consistency=10) - assert isinstance(result, list) - assert len(result) == len(TEST_SEQUENCES) - def test_mode_case_insensitive(self): """Mode names are case-insensitive.""" result = kalign.align(TEST_SEQUENCES, mode="FAST") From 32e555d6cc720e2974ea4cba478cf329dff7e271 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Wed, 18 Mar 2026 18:19:14 +0800 Subject: [PATCH 04/29] POAR consistency merge, Kimura nucleotide matrices, NSGA-III optimized presets for protein/RNA/DNA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major features: - POAR-based consistency merge for ensemble alignment: extracts pairwise residue consistency scores from the POAR table (already built during ensemble) and uses them as bonus weights in a final progressive alignment. Controlled by kalign_ensemble_config.consistency_merge (0=POAR consensus, 1=consistency re-alignment). Tested by NSGA-III optimizer but POAR consensus consistently outperformed it on BAliBASE. - Kimura two-parameter nucleotide substitution matrices (1PAM, 20PAM, 200PAM) with kappa=2 transition/transversion ratio. Gives the optimizer matrix diversity for DNA/RNA, analogous to PFASUM43/60/CB66 for protein. K200 strongly preferred by optimizer for both RNA and DNA. - Farthest-first anchor selection for guide tree distance computation. Replaces length-stratified sampling with BPM-based diversity selection. Performance equivalent on BAliBASE but more principled. - 12 NSGA-III optimized presets: 4 protein (BAliBASE gen 41), 4 RNA (BRAliBASE gen 88), 4 DNA (MDSA gen 100). Each biotype has fast, default, recall, and accurate modes. Nucleotide optimization run (combined BRAliBASE + MDSA) in progress to produce unified presets. Protein presets (BAliBASE, 218 cases): fast: R=0.815 P=0.674 F1=0.732 7s (single P60) default: R=0.786 P=0.722 F1=0.747 40s (single P60, inline refine) recall: R=0.841 P=0.728 F1=0.776 370s (ens5 CB66/P60/P43, realign=1) accurate: R=0.787 P=0.845 F1=0.807 502s (ens5 P43/CB66, realign=1) RNA presets (BRAliBASE, 599 cases — all beat MUSCLE F1=0.825): fast: R=0.832 P=0.825 F1=0.828 5s (single K200) default: R=0.804 P=0.869 F1=0.835 8s (ens3 K200/K20) recall: R=0.833 P=0.826 F1=0.829 6s (single K200) accurate: R=0.811 P=0.863 F1=0.836 26s (ens3 K200/K20, realign=2) DNA presets (MDSA, 325 cases): fast: R=0.741 P=0.788 F1=0.764 18s (ens3 K200/K20, realign=1) default: R=0.737 P=0.816 F1=0.775 35s (ens3 K200/K20, realign=1) recall: R=0.760 P=0.770 F1=0.765 65s (ens5 K200, realign=1) accurate: R=0.737 P=0.816 F1=0.775 35s (=default, optimizer converged) Optimizer changes: - Objectives changed from (F-beta, TC, time) to (recall, precision, time) for richer Pareto fronts - Combined "nucleotide" dataset (BRAliBASE + MDSA) for unified optimization - Checkpoint backfill for consistency_merge and per-run vsm_amax/refine - Dashboard shows recall/precision columns, consistency merge status - Pareto front seed merging from separate RNA + DNA checkpoints Other changes: - kalign_ensemble_config gains consistency_merge and consistency_merge_weight - msa_struct gains poar_consistency void* for non-owning POAR reference - aln_run.c dispatches poar_consistency before anchor_consistency - pick_anchor.h exposes pick_anchor_n() for configurable anchor count - Python __init__.py adds "recall" to _PRESET_MODES Co-Authored-By: Claude Opus 4.6 (1M context) --- benchmarks/bench_modes.py | 63 +++++ benchmarks/datasets.py | 30 ++- benchmarks/eval_checkpoint_configs.py | 113 ++++++++ benchmarks/optimize_unified.py | 358 ++++++++++++++++++++------ benchmarks/view_pareto.py | 40 ++- build.zig | 1 + docs/PRD-msa-consistency.md | 98 +++++++ lib/CMakeLists.txt | 1 + lib/include/kalign/kalign.h | 3 + lib/include/kalign/kalign_config.h | 2 + lib/src/aln_param.c | 38 +++ lib/src/aln_run.c | 47 +++- lib/src/aln_wrap.c | 310 ++++++++++++++++------ lib/src/ensemble.c | 197 ++++++++------ lib/src/msa_alloc.c | 1 + lib/src/msa_consistency.c | 185 +++++++++++++ lib/src/msa_consistency.h | 46 ++++ lib/src/msa_op.c | 1 + lib/src/msa_struct.h | 1 + lib/src/pick_anchor.c | 167 +++++++----- lib/src/pick_anchor.h | 1 + lib/src/sequence_distance.c | 14 +- python-kalign/__init__.py | 2 +- python-kalign/_core.cpp | 15 +- 24 files changed, 1404 insertions(+), 330 deletions(-) create mode 100644 benchmarks/bench_modes.py create mode 100644 benchmarks/eval_checkpoint_configs.py create mode 100644 docs/PRD-msa-consistency.md create mode 100644 lib/src/msa_consistency.c create mode 100644 lib/src/msa_consistency.h diff --git a/benchmarks/bench_modes.py b/benchmarks/bench_modes.py new file mode 100644 index 0000000..b6a38f3 --- /dev/null +++ b/benchmarks/bench_modes.py @@ -0,0 +1,63 @@ +"""Benchmark kalign mode presets (fast/default/accurate) vs external tools on BAliBASE.""" + +import statistics +import time +from pathlib import Path + +from .datasets import get_cases +from .scoring import run_case, EXTERNAL_TOOLS + + +def main(): + cases = get_cases("balibase") + if not cases: + print("No BAliBASE cases found. Run: uv run python -m benchmarks --download-only") + return + + print(f"BAliBASE: {len(cases)} cases\n") + + methods = [ + ("kalign fast", dict(method="python_api", mode="fast")), + ("kalign default", dict(method="python_api", mode="default")), + ("kalign accurate", dict(method="python_api", mode="accurate")), + ("clustalo", dict(method="clustalo")), + ("mafft", dict(method="mafft")), + ("muscle", dict(method="muscle")), + ] + + all_results = {} + for label, kwargs in methods: + print(f"Running {label}...", flush=True) + results = [] + t0 = time.perf_counter() + for i, case in enumerate(cases): + r = run_case(case, n_threads=1, **kwargs) + results.append(r) + if r.error: + print(f" [{i+1}/{len(cases)}] {r.family}: ERROR {r.error}") + elapsed = time.perf_counter() - t0 + + ok = [r for r in results if not r.error] + all_results[label] = ok + if ok: + print(f" {len(ok)} cases, total {elapsed:.1f}s") + print() + + # Summary table + print(f"{'Method':<20} {'N':>4} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time(s)':>10}") + print("-" * 72) + for label, _ in methods: + ok = all_results.get(label, []) + if not ok: + print(f"{label:<20} {'0':>4} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>10}") + continue + rec = statistics.mean([r.recall for r in ok]) + pre = statistics.mean([r.precision for r in ok]) + f1 = statistics.mean([r.f1 for r in ok]) + tc = statistics.mean([r.tc for r in ok]) + t = sum(r.wall_time for r in ok) + print(f"{label:<20} {len(ok):>4} {rec:>8.3f} {pre:>8.3f} {f1:>8.3f} {tc:>8.3f} {t:>10.1f}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/datasets.py b/benchmarks/datasets.py index 5434fe1..776b7f1 100644 --- a/benchmarks/datasets.py +++ b/benchmarks/datasets.py @@ -298,7 +298,8 @@ def balifam_cases() -> List[BenchmarkCase]: # Skip PREFAB (all pairwise, 2 seqs — not useful for MSA benchmarking) MDSA_DATABASES = ["balibase", "oxbench", "smart"] MDSA_VERSION = "all" # "100s" is too small (400 cases); "all" has 1,869 usable cases -MDSA_MAX_SEQS = 500 # Skip very large families (SMART has up to 1,769 seqs) +MDSA_MAX_SEQS = 15 # Cap for optimizer feasibility (~325 cases, ~3-4 days for 100 gens) +MDSA_MAX_ALN_LEN = 400 # Skip families with very long reference alignments def mdsa_download() -> None: @@ -409,6 +410,13 @@ def mdsa_cases() -> List[BenchmarkCase]: # Skip trivial pairwise cases if n_seqs < 3: continue + # Skip families with very long alignments + aln_len = max( + (len(line.strip()) for line in open(ref) if not line.startswith(">")), + default=0, + ) + if aln_len > MDSA_MAX_ALN_LEN: + continue cases.append( BenchmarkCase( @@ -427,6 +435,21 @@ def mdsa_cases() -> List[BenchmarkCase]: # Registry # --------------------------------------------------------------------------- +def _nucleotide_download() -> None: + """Download both BRAliBASE (RNA) and MDSA (DNA).""" + bralibase_download() + mdsa_download() + + +def _nucleotide_is_available() -> bool: + return bralibase_is_available() and mdsa_is_available() + + +def _nucleotide_cases() -> List[BenchmarkCase]: + """Combined BRAliBASE + MDSA cases for unified nucleotide optimization.""" + return bralibase_cases() + mdsa_cases() + + DATASETS = { "balibase": { "download": balibase_download, @@ -448,6 +471,11 @@ def mdsa_cases() -> List[BenchmarkCase]: "is_available": mdsa_is_available, "cases": mdsa_cases, }, + "nucleotide": { + "download": _nucleotide_download, + "is_available": _nucleotide_is_available, + "cases": _nucleotide_cases, + }, } diff --git a/benchmarks/eval_checkpoint_configs.py b/benchmarks/eval_checkpoint_configs.py new file mode 100644 index 0000000..49b67e4 --- /dev/null +++ b/benchmarks/eval_checkpoint_configs.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Evaluate selected configs from an NSGA-III checkpoint on full BAliBASE. + +Reports true SP (recall), precision, F1, TC, and wall time — not CV estimates. +""" + +import pickle +import sys +import time +from pathlib import Path + + +# Ensure the benchmarks package is importable +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from benchmarks.optimize_unified import ( + decode_unified_params, + evaluate_unified, + set_active_profile, +) +from benchmarks.datasets import balibase_cases, balibase_download, balibase_is_available + + +def main(): + checkpoint_path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/gen_checkpoint.pkl" + + with open(checkpoint_path, "rb") as f: + ckpt = pickle.load(f) + + X = ckpt["pop_X"] + F = ckpt["pop_F"] + max_runs = ckpt["max_runs"] + f1_cv = -F[:, 0] + tc_cv = -F[:, 1] + + # Selected configs to evaluate + selected = { + 122: "Hyper-fast bare (n=1,no refine)", + 67: "Fast+refine (n=1,ref=CONF)", + } + + # Set up BAliBASE + profile = ckpt.get("profile", "protein") + set_active_profile(profile) + + if not balibase_is_available(): + print("Downloading BAliBASE...", flush=True) + balibase_download() + + cases = balibase_cases() + print(f"Loaded {len(cases)} BAliBASE cases (profile: {profile})\n") + + # Print header + print(f"{'Idx':>4} {'Label':<25} {'CV_F1':>6} {'CV_TC':>6} " + f"{'Recall':>7} {'Prec':>7} {'F1':>7} {'TC':>7} {'Time':>8}") + print("-" * 95) + + results = {} + for idx, label in selected.items(): + x = X[idx] + params = decode_unified_params(x, max_runs) + + print(f"[{idx:3d}] {label:<25} {f1_cv[idx]:.4f} {tc_cv[idx]:.4f} ", + end="", flush=True) + + start = time.perf_counter() + result = evaluate_unified(params, cases, n_threads=1, quiet=False) + elapsed = time.perf_counter() - start + + print(f"{result['recall']:.4f} {result['precision']:.4f} " + f"{result['f1']:.4f} {result['tc']:.4f} {elapsed:7.1f}s") + + results[idx] = { + "label": label, + "cv_f1": f1_cv[idx], + "cv_tc": tc_cv[idx], + "params": params, + **result, + } + + # Summary table + print("\n" + "=" * 95) + print("FULL BENCHMARK RESULTS vs COMPETITORS") + print("=" * 95) + print(f"\n{'Method':<30} {'Recall(SP)':>10} {'Prec':>7} {'F1':>7} {'TC':>7} {'Time':>8}") + print("-" * 75) + + for idx, label in selected.items(): + r = results[idx] + print(f"kalign [{idx}] {label:<20} {r['recall']:>10.4f} {r['precision']:>7.4f} " + f"{r['f1']:>7.4f} {r['tc']:>7.4f} {r['wall_time']:>7.1f}s") + + print("-" * 75) + print(f"{'kalign fast (shipped)':<30} {'0.809':>10} {'0.663':>7} {'0.723':>7} {'0.482':>7} {'10':>7}s") + print(f"{'kalign default (shipped)':<30} {'0.816':>10} {'0.758':>7} {'0.780':>7} {'0.490':>7} {'101':>7}s") + print(f"{'kalign accurate (shipped)':<30} {'0.837':>10} {'0.719':>7} {'0.769':>7} {'0.518':>7} {'262':>7}s") + print(f"{'ClustalO':<30} {'0.840':>10} {'0.710':>7} {'0.764':>7} {'0.559':>7}") + print(f"{'MAFFT':<30} {'0.867':>10} {'0.715':>7} {'0.778':>7} {'0.590':>7}") + print(f"{'MUSCLE':<30} {'0.870':>10} {'0.721':>7} {'0.783':>7} {'0.581':>7}") + + # Per-category breakdown for each config + for idx, label in selected.items(): + r = results[idx] + if "per_category" in r and r["per_category"]: + print(f"\n--- [{idx}] {label} per-category ---") + for cat in sorted(r["per_category"]): + c = r["per_category"][cat] + print(f" {cat:<20} Recall={c['recall']:.4f} Prec={c['precision']:.4f} " + f"F1={c['f1']:.4f} TC={c['tc']:.4f} (n={c['n']})") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/optimize_unified.py b/benchmarks/optimize_unified.py index 822a351..cb660de 100644 --- a/benchmarks/optimize_unified.py +++ b/benchmarks/optimize_unified.py @@ -3,12 +3,14 @@ Searches across kalign's entire operating range: from fast single-run alignment through consistency-enhanced single-run to multi-run ensemble with POAR consensus. -Always uses three objectives: F1, TC, and wall time. The resulting 3D Pareto -surface reveals the full speed/accuracy trade-off landscape in one run. +Always uses three objectives: recall, precision, and wall time. The resulting 3D +Pareto surface reveals the full speed/accuracy trade-off landscape in one run. +Recall and precision are naturally in tension, yielding a richer Pareto front +than F1+TC. Objectives (always 3): - 1. Maximize F1 (category-averaged, held-out CV) -> pymoo minimizes -F1 - 2. Maximize TC (category-averaged, held-out CV) -> pymoo minimizes -TC + 1. Maximize recall (category-averaged SP score, held-out CV) + 2. Maximize precision (category-averaged, held-out CV) 3. Minimize wall time (total CV evaluation time in seconds) Per-run decision variables (x max_runs slots): @@ -101,6 +103,7 @@ from kalign._core import ( # type: ignore[import-untyped] MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66, MATRIX_DNA, MATRIX_RNA, + MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM, REFINE_NONE, REFINE_ALL, REFINE_CONFIDENT, REFINE_INLINE, ensemble_custom_file_to_file, ) @@ -149,12 +152,12 @@ # seq_weights: 0 by default for RNA, search [0, 3] "shared_cont_lower": np.array([0.0, 0.0, 0.5]), "shared_cont_upper": np.array([3.0, 3.0, 5.0]), - # RNA has only one scoring type - "matrix_map_int": [MATRIX_RNA], - "matrix_map_str": ["rna"], - "matrix_names": {MATRIX_RNA: "RNA"}, - "n_matrices": 1, - "seq_type_int": MATRIX_RNA, + # Kimura 1/20/200 PAM matrices (shared with DNA) + "matrix_map_int": [MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM], + "matrix_map_str": ["nuc_1pam", "nuc_20pam", "nuc_200pam"], + "matrix_names": {MATRIX_NUC_1PAM: "K1", MATRIX_NUC_20PAM: "K20", MATRIX_NUC_200PAM: "K200"}, + "n_matrices": 3, + "seq_type_int": MATRIX_NUC_200PAM, "seq_type_str": "rna", "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range }, @@ -163,14 +166,28 @@ "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), "shared_cont_lower": np.array([0.0, 0.0, 0.5]), "shared_cont_upper": np.array([3.0, 3.0, 5.0]), - "matrix_map_int": [MATRIX_DNA], - "matrix_map_str": ["dna"], - "matrix_names": {MATRIX_DNA: "DNA"}, - "n_matrices": 1, - "seq_type_int": MATRIX_DNA, + # Kimura 1/20/200 PAM matrices (shared with RNA) + "matrix_map_int": [MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM], + "matrix_map_str": ["nuc_1pam", "nuc_20pam", "nuc_200pam"], + "matrix_names": {MATRIX_NUC_1PAM: "K1", MATRIX_NUC_20PAM: "K20", MATRIX_NUC_200PAM: "K200"}, + "n_matrices": 3, + "seq_type_int": MATRIX_NUC_200PAM, "seq_type_str": "dna", "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range }, + "nucleotide": { + "per_run_cont_lower": np.array([1.0, 0.2, 0.05, 0.0]), + "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), + "shared_cont_lower": np.array([0.0, 0.0, 0.5]), + "shared_cont_upper": np.array([3.0, 3.0, 5.0]), + "matrix_map_int": [MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM], + "matrix_map_str": ["nuc_1pam", "nuc_20pam", "nuc_200pam"], + "matrix_names": {MATRIX_NUC_1PAM: "K1", MATRIX_NUC_20PAM: "K20", MATRIX_NUC_200PAM: "K200"}, + "n_matrices": 3, + "seq_type_int": MATRIX_NUC_200PAM, + "seq_type_str": "nucleotide", + "max_consistency_idx": len(CONSISTENCY_MAP) - 1, + }, } # Active profile (set by main() based on --dataset) @@ -188,7 +205,10 @@ def _matrix_names(): return _active_profile["matrix_names"] # Legacy aliases (used in view_pareto.py etc.) MATRIX_MAP_INT = PARAM_PROFILES["protein"]["matrix_map_int"] MATRIX_MAP_STR = PARAM_PROFILES["protein"]["matrix_map_str"] -MATRIX_NAMES = PARAM_PROFILES["protein"]["matrix_names"] +# Combined matrix names across all profiles (used by dashboard) +MATRIX_NAMES = {} +for _prof in PARAM_PROFILES.values(): + MATRIX_NAMES.update(_prof["matrix_names"]) # Old constant names used in view_pareto.py and checkpoint data — keep for loading old checkpoints _OLD_MATRIX_COMPAT = {3: MATRIX_PFASUM43, 5: MATRIX_PFASUM43, 6: MATRIX_PFASUM60} @@ -231,6 +251,8 @@ def get_vars(max_runs: int) -> dict: variables["consistency"] = Choice(options=consistency_options) variables["realign"] = Integer(bounds=(0, 2)) variables["min_support"] = Integer(bounds=(0, max_runs)) + variables["consistency_merge"] = Choice(options=[0, 1]) + variables["consistency_merge_weight"] = Real(bounds=(0.5, 10.0)) return variables @@ -249,6 +271,8 @@ def decode_unified_params(x, max_runs: int): consistency = int(x["consistency"]) realign = int(x["realign"]) min_support_raw = int(x["min_support"]) + consistency_merge = int(x.get("consistency_merge", 0)) + consistency_merge_weight = float(x.get("consistency_merge_weight", 2.0)) seq_weights = float(x["seq_weights"]) consistency_weight = float(x["consistency_weight"]) @@ -291,6 +315,14 @@ def decode_unified_params(x, max_runs: int): if consistency == 0: consistency_weight = 1.0 + # Single-run: no consistency merge (needs ensemble) + if n_runs == 1: + consistency_merge = 0 + + # When consistency_merge == 0, weight is irrelevant + if consistency_merge == 0: + consistency_merge_weight = 2.0 + return { "n_runs": n_runs, "run_gpo": run_gpo, @@ -306,6 +338,8 @@ def decode_unified_params(x, max_runs: int): "consistency": consistency, "realign": realign, "min_support": min_support, + "consistency_merge": consistency_merge, + "consistency_merge_weight": consistency_merge_weight, } @@ -336,6 +370,8 @@ def encode_unified_params(params, max_runs: int) -> dict: x["consistency"] = params["consistency"] x["realign"] = params["realign"] x["min_support"] = params["min_support"] + x["consistency_merge"] = params.get("consistency_merge", 0) + x["consistency_merge_weight"] = params.get("consistency_merge_weight", 2.0) return x @@ -359,9 +395,10 @@ def format_unified_short(params): # Show per-run refine modes compactly refs = "/".join(REFINE_NAMES.get(r, "?") for r in params["run_refine"]) vsms = "/".join(f"{v:.1f}" for v in params["run_vsm_amax"]) + cm = "CM" if params.get("consistency_merge", 0) else "POAR" return (f"{mode_label(params)} vsm={vsms} " f"sw={params['seq_weights']:.1f} c={params['consistency']} " - f"re={params['realign']} ref={refs} ms={params['min_support']}") + f"re={params['realign']} ref={refs} ms={params['min_support']} {cm}") def format_unified_long(params): @@ -380,6 +417,9 @@ def format_unified_long(params): f"consistency_weight={params['consistency_weight']:.3f}") lines.append(f" realign={params['realign']} " f"min_support={params['min_support']}") + if params.get("consistency_merge", 0): + lines.append(f" consistency_merge=1 " + f"consistency_merge_weight={params['consistency_merge_weight']:.3f}") return "\n".join(lines) @@ -451,6 +491,9 @@ def evaluate_unified(params, cases, n_threads=1, quiet=True): # Per-run overrides run_vsm_amax=params["run_vsm_amax"], run_refine=params["run_refine"], + # MSA consistency merge + consistency_merge=params.get("consistency_merge", 0), + consistency_merge_weight=params.get("consistency_merge_weight", 2.0), ) wall_time = time.perf_counter() - start @@ -496,21 +539,35 @@ def evaluate_unified(params, cases, n_threads=1, quiet=True): } +def fbeta_score(precision, recall, beta=1.0): + """Compute F-beta score: weights recall beta times more than precision.""" + if precision + recall == 0: + return 0.0 + b2 = beta * beta + return (1 + b2) * precision * recall / (b2 * precision + recall) + + def evaluate_cv(params, folds, n_threads=1, quiet=True): """Evaluate unified params using stratified k-fold CV.""" fold_f1s = [] fold_tcs = [] + fold_recalls = [] + fold_precisions = [] total_time = 0.0 for _, test in folds: result = evaluate_unified(params, test, n_threads, quiet) fold_f1s.append(result["f1"]) fold_tcs.append(result["tc"]) + fold_recalls.append(result["recall"]) + fold_precisions.append(result["precision"]) total_time += result["wall_time"] return { "f1": float(np.mean(fold_f1s)), "tc": float(np.mean(fold_tcs)), + "recall": float(np.mean(fold_recalls)), + "precision": float(np.mean(fold_precisions)), "f1_std": float(np.std(fold_f1s)), "tc_std": float(np.std(fold_tcs)), "fold_f1s": fold_f1s, @@ -629,30 +686,28 @@ def _build_best_panel(self): lines = [] if self.best_f1_entry: e = self.best_f1_entry - bl_f1 = max(bl["f1"] for bl in self.baselines.values()) if self.baselines else 0 - delta = e["f1"] - bl_f1 lines.append(self._format_best_entry( - f"Best F1: {e['f1']:.4f} TC={e['tc']:.4f}", e, - f"({delta:+.4f} vs best baseline)")) + f"Best Recall: {e.get('recall', 0):.4f} F1={e['f1']:.4f} " + f"P={e.get('precision', 0):.3f} TC={e.get('tc', 0):.4f}", e, "")) if self.best_tc_entry: e = self.best_tc_entry - bl_tc = max(bl["tc"] for bl in self.baselines.values()) if self.baselines else 0 - delta = e["tc"] - bl_tc lines.append(self._format_best_entry( - f"Best TC: {e['tc']:.4f} F1={e['f1']:.4f}", e, - f"({delta:+.4f} vs best baseline)")) + f"Best Prec: {e.get('precision', 0):.4f} F1={e['f1']:.4f} " + f"R={e.get('recall', 0):.3f} TC={e.get('tc', 0):.4f}", e, "")) if self.fastest_entry: e = self.fastest_entry lines.append(self._format_best_entry( - f"Fastest: {e['wall_time']:.1f}s F1={e['f1']:.4f}", e, "")) + f"Fastest: {e['wall_time']:.1f}s F1={e['f1']:.4f} " + f"R={e.get('recall', 0):.3f} P={e.get('precision', 0):.3f}", e, "")) return Panel("\n".join(lines) if lines else "(no data yet)", title="Best by Objective") def _build_pareto_table(self): table = Table(title="Pareto Front (top 12 by F1)", box=None, padding=(0, 1)) table.add_column("#", style="dim", width=2) table.add_column("Mode", width=6) + table.add_column("Recall", justify="right", width=6) + table.add_column("Prec", justify="right", width=6) table.add_column("F1", justify="right", width=6) - table.add_column("TC", justify="right", width=6) table.add_column("Time", justify="right", width=5) table.add_column("c", width=2) table.add_column("re", width=2) @@ -660,13 +715,13 @@ def _build_pareto_table(self): table.add_column("Params", no_wrap=True) sorted_front = sorted(self.pareto_front, key=lambda x: -x["f1"])[:12] - bl_f1 = max(bl["f1"] for bl in self.baselines.values()) if self.baselines else 0 - bl_tc = max(bl["tc"] for bl in self.baselines.values()) if self.baselines else 0 + bl_recall = max(bl.get("recall", 0) for bl in self.baselines.values()) if self.baselines else 0 + bl_prec = max(bl.get("precision", 0) for bl in self.baselines.values()) if self.baselines else 0 for i, entry in enumerate(sorted_front): p = entry.get("params", {}) - f1_style = "bold green" if entry["f1"] > bl_f1 else "" - tc_style = "bold green" if entry["tc"] > bl_tc else "" + r_style = "bold green" if entry.get("recall", 0) > bl_recall else "" + p_style = "bold green" if entry.get("precision", 0) > bl_prec else "" refs = "/".join(REFINE_NAMES.get(r, "?") for r in p.get("run_refine", [0])) # Build detailed params string @@ -677,15 +732,17 @@ def _build_pareto_table(self): run_strs.append(f"R{k}:{p['run_gpo'][k]:.1f}/{p['run_gpe'][k]:.2f}/" f"{p['run_tgpe'][k]:.2f}/{mat}/v{vsm:.1f}") runs = " ".join(run_strs) + cm = " CM" if p.get("consistency_merge", 0) else "" shared = (f"sw={p.get('seq_weights', 0):.1f} " - f"ms={p.get('min_support', 0)}") + f"ms={p.get('min_support', 0)}{cm}") params_str = f"{runs} | {shared}" table.add_row( str(i), mode_label(p), - Text(f"{entry['f1']:.4f}", style=f1_style), - Text(f"{entry['tc']:.4f}", style=tc_style), + Text(f"{entry.get('recall', 0):.4f}", style=r_style), + Text(f"{entry.get('precision', 0):.4f}", style=p_style), + f"{entry['f1']:.4f}", f"{entry.get('wall_time', 0):.0f}s", str(p.get("consistency", 0)), str(p.get("realign", 0)), @@ -707,7 +764,9 @@ def _build_trend_panel(self): lines = [] for h in self.gen_history[-6:]: lines.append(f"Gen {h['gen']:3d}: F1={h['best_f1']:.4f} " - f"TC={h['best_tc']:.4f} n_pareto={h['n_pareto']}") + f"R={h.get('best_recall', 0):.4f} " + f"P={h.get('best_prec', 0):.4f} " + f"n_pareto={h['n_pareto']}") return Panel("\n".join(lines) if lines else "(no data)", title="Trend") def _build_recent_panel(self): @@ -760,20 +819,23 @@ def on_eval_start(self, params: dict, eval_num: int, eval_in_gen: int): self.refresh() def on_eval_end(self, params: dict, cv_result: dict): - f1 = cv_result["f1"] - tc = cv_result["tc"] + recall = cv_result.get("recall", 0.0) + precision = cv_result.get("precision", 0.0) + f1 = fbeta_score(precision, recall, 1.0) + tc = cv_result.get("tc", 0.0) wt = cv_result.get("wall_time", 0.0) - entry = {"params": params, "f1": f1, "tc": tc, "wall_time": wt} + entry = {"params": params, "f1": f1, "tc": tc, "wall_time": wt, + "recall": recall, "precision": precision} self.recent_evals.append(entry) if len(self.recent_evals) > 5: self.recent_evals.pop(0) - if f1 > self.best_f1: - self.best_f1 = f1 + if recall > self.best_f1: # reuse field for best recall + self.best_f1 = recall self.best_f1_entry = entry - if tc > self.best_tc: - self.best_tc = tc + if precision > self.best_tc: # reuse field for best precision + self.best_tc = precision self.best_tc_entry = entry if wt < self.fastest and f1 > 0.5: self.fastest = wt @@ -791,12 +853,14 @@ def on_gen_start(self, gen: int): def on_gen_end(self, gen: int, pareto_front: List[dict]): self.pareto_front = pareto_front + best_recall = max((s.get("recall", 0) for s in pareto_front), default=0.0) + best_prec = max((s.get("precision", 0) for s in pareto_front), default=0.0) best_f1_in_gen = max((s["f1"] for s in pareto_front), default=0.0) - best_tc_in_gen = max((s["tc"] for s in pareto_front), default=0.0) self.gen_history.append({ "gen": gen, "best_f1": best_f1_in_gen, - "best_tc": best_tc_in_gen, + "best_recall": best_recall, + "best_prec": best_prec, "n_pareto": len(pareto_front), }) @@ -845,16 +909,17 @@ class UnifiedCVProblem(Problem): """3-objective optimization with stratified CV evaluation.""" def __init__(self, folds, max_runs: int, n_threads=1, n_workers=1, - dashboard: Optional[Dashboard] = None): + dashboard: Optional[Dashboard] = None, f_beta: float = 1.0): super().__init__( vars=get_vars(max_runs), - n_obj=3, # always 3: -F1, -TC, time + n_obj=3, # always 3: -recall, -precision, time ) self.folds = folds self.max_runs = max_runs self.n_threads = n_threads self.n_workers = n_workers self.dashboard = dashboard + self.f_beta = f_beta self.eval_count = 0 self.history: List[dict] = [] @@ -910,12 +975,16 @@ def _evaluate_parallel(self, X, F): fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] + fold_recalls = [fold_results[ind_idx][fi]["recall"] for fi in range(n_folds)] + fold_precisions = [fold_results[ind_idx][fi]["precision"] for fi in range(n_folds)] total_time = sum(fold_results[ind_idx][fi]["wall_time"] for fi in range(n_folds)) cv_result = { "f1": float(np.mean(fold_f1s)), "tc": float(np.mean(fold_tcs)), + "recall": float(np.mean(fold_recalls)), + "precision": float(np.mean(fold_precisions)), "f1_std": float(np.std(fold_f1s)), "tc_std": float(np.std(fold_tcs)), "fold_f1s": fold_f1s, @@ -936,8 +1005,10 @@ def _evaluate_parallel(self, X, F): pool.shutdown(wait=False) def _record(self, i, F, params, cv_result): - F[i, 0] = -cv_result["f1"] - F[i, 1] = -cv_result["tc"] + fb = fbeta_score(cv_result["precision"], cv_result["recall"], self.f_beta) + cv_result["fbeta"] = fb + F[i, 0] = -cv_result["recall"] + F[i, 1] = -cv_result["precision"] F[i, 2] = cv_result["wall_time"] self.history.append({ @@ -974,10 +1045,15 @@ def notify(self, algorithm): if algorithm.opt is not None and len(algorithm.opt) > 0: for ind in algorithm.opt: params = decode_unified_params(ind.X, self.max_runs) + recall = -ind.F[0] + precision = -ind.F[1] + f1 = fbeta_score(precision, recall, 1.0) entry = { "params": params, - "f1": -ind.F[0], - "tc": -ind.F[1], + "recall": recall, + "precision": precision, + "f1": f1, + "tc": 0.0, # not an objective; filled from cv_result if available "wall_time": ind.F[2], } pareto.append(entry) @@ -1002,6 +1078,7 @@ def notify(self, algorithm): "pop_size": len(pop), "max_runs": self.max_runs, "profile": _active_profile.get("seq_type_str", "protein"), + "f_beta": self.problem.f_beta if self.problem else 1.0, } tmp = self.checkpoint_path.with_suffix(".tmp") with open(tmp, "wb") as f: @@ -1010,9 +1087,39 @@ def notify(self, algorithm): def load_checkpoint(path: Path): - """Load a generation checkpoint.""" + """Load a generation checkpoint. + + Backfills missing keys for new decision variables so old checkpoints + remain compatible with the current variable space. + """ with open(path, "rb") as f: ckpt = pickle.load(f) # noqa: S301 + + # Backfill new variables added after the checkpoint was saved. + # Each individual in pop_X is a dict of decision variables. + # pymoo's crossover directly indexes parent.X[var_name], so ALL + # variables in get_vars() must exist in every individual dict. + max_runs = ckpt.get("max_runs", 5) + pop_X = ckpt.get("pop_X") + if pop_X is not None: + for x in pop_X: + if not isinstance(x, dict): + continue + + # v1 → v2: shared vsm_amax/refine → per-run arrays + if "vsm_amax_0" not in x: + shared_vsm = float(x.get("vsm_amax", 0.0)) + shared_ref = int(x.get("refine", REFINE_NONE)) + for k in range(max_runs): + x[f"vsm_amax_{k}"] = shared_vsm + x[f"refine_{k}"] = shared_ref + + # MSA consistency merge (added v3.6) + if "consistency_merge" not in x: + x["consistency_merge"] = 0 + if "consistency_merge_weight" not in x: + x["consistency_merge_weight"] = 2.0 + return ckpt @@ -1112,7 +1219,7 @@ def get_baseline_configs(dataset: str) -> dict: """Return baseline configs appropriate for the dataset.""" if dataset == "bralibase": return BASELINE_CONFIGS_RNA - if dataset == "mdsa": + if dataset in ("mdsa", "nucleotide"): return BASELINE_CONFIGS_DNA return BASELINE_CONFIGS_PROTEIN @@ -1147,11 +1254,17 @@ def main(): help="Name for this run (creates subdirectory)") parser.add_argument("--no-dashboard", action="store_true", help="Disable rich dashboard, use plain text output") + parser.add_argument("--f-beta", type=float, default=1.0, + help="F-beta weight for recall vs precision. " + "1.0=standard F1, 1.5=recall-weighted, 2.0=F2 (default: 1.0)") parser.add_argument("--dataset", type=str, default="balibase", - choices=["balibase", "bralibase", "mdsa"], + choices=["balibase", "bralibase", "mdsa", "nucleotide"], help="Benchmark dataset (default: balibase)") parser.add_argument("--resume", type=str, default=None, help="Resume from a generation checkpoint file (.pkl)") + parser.add_argument("--seed-from", type=str, default=None, + help="Seed initial population from another checkpoint (e.g. a beta=1.0 run). " + "Parameters are kept, fitness is re-evaluated with current settings.") args = parser.parse_args() console = Console() @@ -1175,10 +1288,45 @@ def main(): console.print("Downloading MDSA...") mdsa_download() cases = mdsa_cases() + elif args.dataset == "nucleotide": + set_active_profile("nucleotide") + if not bralibase_is_available(): + console.print("Downloading BRAliBASE...") + bralibase_download() + if not mdsa_is_available(): + console.print("Downloading MDSA...") + mdsa_download() + cases = bralibase_cases() + mdsa_cases() + console.print(f" Combined: {len([c for c in cases if c.seq_type == 'rna'])} RNA + " + f"{len([c for c in cases if c.seq_type == 'dna'])} DNA cases") else: console.print(f"[bold red]Unknown dataset: {args.dataset}[/]") return + # --- Run configuration banner --- + variables = get_vars(args.max_runs) + var_names = sorted(variables.keys()) + has_cm = "consistency_merge" in variables + console.print() + console.print("[bold cyan]=" * 72) + console.print("[bold cyan] Kalign Unified Optimizer") + console.print("[bold cyan]=" * 72) + console.print(f" Dataset: [bold]{args.dataset}[/] ({_active_profile['seq_type_str']})") + console.print(f" Pop size: {args.pop_size} Generations: {args.n_gen}") + console.print(f" Workers: {args.n_workers} Threads/eval: {args.n_threads}") + console.print(f" Max runs: {args.max_runs}") + console.print(f" Objectives: recall, precision, time") + console.print(f" Variables: {len(var_names)}") + console.print(f" Resume: {args.resume or 'no'}") + console.print(f" Seed from: {args.seed_from or 'no'}") + cm_status = "[bold green]ENABLED[/]" if has_cm else "[bold red]DISABLED[/]" + console.print(f" Consistency merge: {cm_status}") + if has_cm: + console.print(f" - consistency_merge: Choice([0, 1])") + console.print(f" - consistency_merge_weight: Real([0.5, 10.0])") + console.print("[bold cyan]" + "-" * 72) + console.print() + console.print(f"Loaded [bold]{len(cases)}[/] {args.dataset} cases " f"(profile: {_active_profile['seq_type_str']})") @@ -1208,8 +1356,6 @@ def main(): output_dir.mkdir(parents=True, exist_ok=True) max_runs = args.max_runs - n_var = len(get_vars(max_runs)) - console.print(f"\nDecision vector: {n_var} variables (max_runs={max_runs})") # --- Baseline evaluations (parallelized) --- bl_configs = get_baseline_configs(args.dataset) @@ -1282,12 +1428,17 @@ def main(): max_runs=max_runs, ) + if args.f_beta != 1.0: + console.print(f"[bold yellow]Using F-beta objective with β={args.f_beta}[/] " + f"(recall weighted {args.f_beta}x more than precision)") + problem = UnifiedCVProblem( folds=folds, max_runs=max_runs, n_threads=args.n_threads, n_workers=args.n_workers, dashboard=dashboard, + f_beta=args.f_beta, ) checkpoint_path = output_dir / "gen_checkpoint.pkl" @@ -1368,10 +1519,46 @@ def main(): if pop_size < n_ref: console.print(f"[bold yellow]Warning:[/] pop_size ({pop_size}) < reference " f"directions ({n_ref}). Consider --pop-size {n_ref} or larger.") + + # Seed from another checkpoint (parameters only, fitness will be re-evaluated) + seed_sampling = MixedVariableSampling() + if args.seed_from: + seed_path = Path(args.seed_from) + if not seed_path.exists(): + console.print(f"[bold red]Seed checkpoint not found:[/] {seed_path}") + return + seed_ckpt = load_checkpoint(seed_path) + seed_X = seed_ckpt["pop_X"] + seed_beta = seed_ckpt.get("f_beta", 1.0) + console.print(f"[bold green]Seeding[/] initial population with {len(seed_X)} " + f"individuals from {seed_path.name} (was β={seed_beta})") + # Take up to pop_size individuals, sorted by original fitness (best first) + seed_F = seed_ckpt["pop_F"] + # Sort by first objective (best = most negative) + order = np.argsort(seed_F[:, 0]) + seed_X = seed_X[order][:pop_size] + + # Inject diversity for new binary variables: flip ~25% of + # ensemble individuals to consistency_merge=1 so the optimizer + # can compare both paths from the start. + n_flipped = 0 + for idx, x in enumerate(seed_X): + if not isinstance(x, dict): + continue + if int(x.get("n_runs", 1)) > 1 and idx % 4 == 0: + x["consistency_merge"] = 1 + n_flipped += 1 + if n_flipped: + console.print(f" Flipped {n_flipped} individuals to consistency_merge=1") + + # Create unevaluated population — pymoo will evaluate with new f_beta + seed_pop = Population.new("X", seed_X) + seed_sampling = seed_pop + algorithm = NSGA3( ref_dirs=ref_dirs, pop_size=pop_size, - sampling=MixedVariableSampling(), + sampling=seed_sampling, mating=mixed_mating, eliminate_duplicates=mixed_dedup, ) @@ -1407,32 +1594,38 @@ def main(): pareto_configs = [] for i, (x, f) in enumerate(zip(res.X, res.F)): params = decode_unified_params(x, max_runs) - f1 = -f[0] - tc = -f[1] + recall = -f[0] + precision = -f[1] wt = f[2] - pareto_configs.append({"params": params, "f1_cv": f1, "tc_cv": tc, "wall_time": wt}) + f1 = fbeta_score(precision, recall, 1.0) + pareto_configs.append({ + "params": params, "recall_cv": recall, "prec_cv": precision, + "f1_cv": f1, "wall_time": wt, + }) # Print Pareto front table = Table(title="Pareto Front (sorted by CV F1)") table.add_column("#", style="dim", width=3) table.add_column("Mode", width=6) - table.add_column("CV F1", justify="right") - table.add_column("CV TC", justify="right") + table.add_column("Recall", justify="right") + table.add_column("Prec", justify="right") + table.add_column("F1", justify="right") table.add_column("Time", justify="right") table.add_column("Parameters") sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) - bl_best_f1 = max(bl["f1"] for bl in baselines.values()) - bl_best_tc = max(bl["tc"] for bl in baselines.values()) + bl_best_recall = max(bl.get("recall", 0) for bl in baselines.values()) + bl_best_prec = max(bl.get("precision", 0) for bl in baselines.values()) for i, cfg in enumerate(sorted_pareto[:30]): - f1_style = "bold green" if cfg["f1_cv"] > bl_best_f1 else "" - tc_style = "bold green" if cfg["tc_cv"] > bl_best_tc else "" + r_style = "bold green" if cfg.get("recall_cv", 0) > bl_best_recall else "" + p_style = "bold green" if cfg.get("prec_cv", 0) > bl_best_prec else "" table.add_row( str(i), mode_label(cfg["params"]), - Text(f"{cfg['f1_cv']:.4f}", style=f1_style), - Text(f"{cfg['tc_cv']:.4f}", style=tc_style), + Text(f"{cfg.get('recall_cv', 0):.4f}", style=r_style), + Text(f"{cfg.get('prec_cv', 0):.4f}", style=p_style), + f"{cfg['f1_cv']:.4f}", f"{cfg['wall_time']:.0f}s", format_unified_short(cfg["params"]), ) @@ -1456,11 +1649,10 @@ def main(): for i, cfg in enumerate(top3): full_result = evaluate_unified(cfg["params"], cases, args.n_threads) console.print(f"\n [{i}] {mode_label(cfg['params'])} " - f"CV F1={cfg['f1_cv']:.4f} -> Full F1={full_result['f1']:.4f} " - f"CV TC={cfg['tc_cv']:.4f} -> Full TC={full_result['tc']:.4f}") + f"CV R={cfg.get('recall_cv', 0):.4f} P={cfg.get('prec_cv', 0):.4f} " + f"F1={cfg['f1_cv']:.4f} -> Full F1={full_result['f1']:.4f}") gap_f1 = full_result["f1"] - cfg["f1_cv"] - gap_tc = full_result["tc"] - cfg["tc_cv"] - console.print(f" Overfit check: F1 {gap_f1:+.4f} TC {gap_tc:+.4f}") + console.print(f" Overfit check: F1 {gap_f1:+.4f}") for cat, v in sorted(full_result["per_category"].items()): console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") console.print(f" {format_unified_long(cfg['params'])}") @@ -1474,7 +1666,8 @@ def main(): if fast_candidates: best_fast = fast_candidates[0] console.print(f"\n [bold]Fast[/] (< 15s): F1={best_fast['f1_cv']:.4f} " - f"TC={best_fast['tc_cv']:.4f} Time={best_fast['wall_time']:.0f}s") + f"R={best_fast.get('recall_cv', 0):.4f} P={best_fast.get('prec_cv', 0):.4f} " + f"Time={best_fast['wall_time']:.0f}s") console.print(f" {format_unified_short(best_fast['params'])}") # Default: best F1 among solutions under 60s @@ -1482,13 +1675,15 @@ def main(): if default_candidates: best_default = default_candidates[0] console.print(f"\n [bold]Default[/] (< 60s): F1={best_default['f1_cv']:.4f} " - f"TC={best_default['tc_cv']:.4f} Time={best_default['wall_time']:.0f}s") + f"R={best_default.get('recall_cv', 0):.4f} P={best_default.get('prec_cv', 0):.4f} " + f"Time={best_default['wall_time']:.0f}s") console.print(f" {format_unified_short(best_default['params'])}") # Accurate: best F1 overall best_overall = sorted_pareto[0] console.print(f"\n [bold]Accurate[/] (best F1): F1={best_overall['f1_cv']:.4f} " - f"TC={best_overall['tc_cv']:.4f} Time={best_overall['wall_time']:.0f}s") + f"R={best_overall.get('recall_cv', 0):.4f} P={best_overall.get('prec_cv', 0):.4f} " + f"Time={best_overall['wall_time']:.0f}s") console.print(f" {format_unified_short(best_overall['params'])}") # --- Save --- @@ -1509,7 +1704,7 @@ def main(): summary_path = output_dir / "pareto_front.txt" with open(summary_path, "w") as f: - f.write(f"# Unified kalign optimization (NSGA-II, 3 objectives: F1, TC, time)\n") + f.write(f"# Unified kalign optimization (NSGA-III, 3 objectives: recall, precision, time)\n") f.write(f"# pop_size={args.pop_size} n_gen={args.n_gen} max_runs={max_runs} " f"n_folds={k} seed={args.seed}\n") f.write(f"# Baselines:\n") @@ -1520,8 +1715,11 @@ def main(): for i, cfg in enumerate(sorted_pareto): p = cfg["params"] - f.write(f"[{i}] mode={mode_label(p)} CV_F1={cfg['f1_cv']:.4f} " - f"CV_TC={cfg['tc_cv']:.4f} Time={cfg['wall_time']:.0f}s\n") + f.write(f"[{i}] mode={mode_label(p)} " + f"R={cfg.get('recall_cv', cfg.get('f1_cv', 0)):.4f} " + f"P={cfg.get('prec_cv', cfg.get('tc_cv', 0)):.4f} " + f"F1={cfg.get('f1_cv', 0):.4f} " + f"Time={cfg['wall_time']:.0f}s\n") f.write(f" n_runs={p['n_runs']}\n") for run_k in range(p["n_runs"]): mat = MATRIX_NAMES.get(p["run_types"][run_k], "?") @@ -1535,7 +1733,11 @@ def main(): f.write(f" consistency={p['consistency']} " f"consistency_weight={p['consistency_weight']:.3f}\n") f.write(f" realign={p['realign']} " - f"min_support={p['min_support']}\n\n") + f"min_support={p['min_support']}\n") + if p.get("consistency_merge", 0): + f.write(f" consistency_merge=1 " + f"consistency_merge_weight={p['consistency_merge_weight']:.3f}\n") + f.write("\n") console.print(f"Pareto front saved to {summary_path}") diff --git a/benchmarks/view_pareto.py b/benchmarks/view_pareto.py index 0b2b606..d4f4f3f 100644 --- a/benchmarks/view_pareto.py +++ b/benchmarks/view_pareto.py @@ -62,12 +62,12 @@ def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: """Build a DataFrame from checkpoint population. Returns (DataFrame, params_by_idx) where params_by_idx maps idx -> full decoded params. - Supports both mixed_v1 (dict-per-individual) and legacy (float array) checkpoints. + Supports mixed_v1 and mixed_v2 (dict-per-individual) checkpoints. """ - if ckpt.get("format") != "mixed_v1": + if ckpt.get("format") not in ("mixed_v1", "mixed_v2"): raise ValueError( "Old-format checkpoint (float arrays). " - "Re-run the optimizer to generate a mixed_v1 checkpoint." + "Re-run the optimizer to generate a mixed_v1/v2 checkpoint." ) pop_X = ckpt["pop_X"] pop_F = ckpt["pop_F"] @@ -100,6 +100,13 @@ def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: # Summarize per-run refine/vsm for the table refs = "/".join(REFINE_LONG.get(r, "?") for r in params["run_refine"]) + # Per-run summaries for hover + vsm_summary = "/".join(f"{v:.1f}" for v in params["run_vsm_amax"]) + mat_summary = "/".join( + MATRIX_NAMES.get(params["run_types"][k], "?") + for k in range(params["n_runs"]) + ) + rows.append({ "idx": i, "f1": round(f1, 4), @@ -107,15 +114,17 @@ def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: "wall_time": round(wt, 1), "mode": mode, "n_runs": params["n_runs"], - "vsm_amax_0": round(params["run_vsm_amax"][0], 3), + "vsm": vsm_summary, + "refine": refs, + "matrices": mat_summary, "seq_weights": round(params["seq_weights"], 3), "consistency": params["consistency"], "consistency_weight": round(params["consistency_weight"], 3), "realign": params["realign"], - "refine": refs, "min_support": params["min_support"], "run_details": "\n".join(run_details), - # Flattened run 0 params for color/hover + # Keep run-0 values for coloring + "vsm_amax_0": round(params["run_vsm_amax"][0], 3), "gpo_0": round(params["run_gpo"][0], 2), "gpe_0": round(params["run_gpe"][0], 2), "tgpe_0": round(params["run_tgpe"][0], 2), @@ -311,7 +320,7 @@ def _format_tier_config(row) -> str: lines.append(f"# {line}") lines.append(f"config = {{") lines.append(f' "n_runs": {row["n_runs"]},') - lines.append(f' "vsm_amax_0": {row["vsm_amax_0"]},') + lines.append(f' "vsm": "{row["vsm"]}", # per-run') lines.append(f' "seq_weights": {row["seq_weights"]},') lines.append(f' "consistency": {row["consistency"]},') lines.append(f' "consistency_weight": {row["consistency_weight"]},') @@ -344,7 +353,7 @@ def create_app(ckpt_path: str, remote_path: str = "", refresh_sec: int = 30, id="color-by", options=[ {"label": "Mode", "value": "mode"}, - {"label": "VSM amax", "value": "vsm_amax_0"}, + {"label": "VSM amax (R0)", "value": "vsm_amax_0"}, {"label": "Seq weights", "value": "seq_weights"}, {"label": "Consistency", "value": "consistency"}, {"label": "Realign", "value": "realign"}, @@ -469,7 +478,7 @@ def create_app(ckpt_path: str, remote_path: str = "", refresh_sec: int = 30, {"name": "F1", "id": "f1"}, {"name": "TC", "id": "tc"}, {"name": "Time", "id": "wall_time"}, - {"name": "VSM", "id": "vsm_amax_0"}, + {"name": "VSM", "id": "vsm"}, {"name": "SW", "id": "seq_weights"}, {"name": "C", "id": "consistency"}, {"name": "CW", "id": "consistency_weight"}, @@ -521,7 +530,8 @@ def _load_data(ckpt_path_arg, remote_path_arg, max_runs_arg): hist_df = build_history_df(ckpt, mr) n_gen = ckpt.get("n_gen_completed", "?") - app._df_cache = {"df": df, "hist_df": hist_df, "mtime": mtime, "n_gen": n_gen, "params_by_idx": params_by_idx} # type: ignore[attr-defined] + f_beta = ckpt.get("f_beta", 1.0) + app._df_cache = {"df": df, "hist_df": hist_df, "mtime": mtime, "n_gen": n_gen, "params_by_idx": params_by_idx, "f_beta": f_beta} # type: ignore[attr-defined] return df, hist_df, mtime @app.callback( @@ -557,15 +567,17 @@ def update_all(n_intervals, color_by, x_axis, y_axis, mode_filter, # Status n_gen = app._df_cache.get("n_gen", "?") + f_beta = app._df_cache.get("f_beta", 1.0) + obj_label = f"F{f_beta}" if f_beta != 1.0 else "F1" ago = time.time() - mtime if mtime else 0 status = (f"Generation {n_gen} | {len(df)} individuals | " - f"Best F1={df['f1'].max():.4f} | Best TC={df['tc'].max():.4f} | " + f"Best {obj_label}={df['f1'].max():.4f} | Best TC={df['tc'].max():.4f} | " f"Last update: {ago:.0f}s ago") # 2D scatter - hover_data = ["mode", "vsm_amax_0", "seq_weights", "consistency", - "realign", "refine", "gpo_0", "gpe_0", "tgpe_0", "matrix_0", - "min_support"] + hover_data = ["mode", "f1", "tc", "wall_time", + "n_runs", "vsm", "refine", "matrices", + "seq_weights", "consistency", "realign", "min_support"] fig2d = px.scatter( filtered, x=x_axis, y=y_axis, color=color_by, hover_data=hover_data, diff --git a/build.zig b/build.zig index 63f80b8..de7b607 100644 --- a/build.zig +++ b/build.zig @@ -111,6 +111,7 @@ const kalign_lib_sources = [_][]const u8{ "lib/src/poar.c", "lib/src/consensus_msa.c", "lib/src/anchor_consistency.c", + "lib/src/msa_consistency.c", "lib/src/ensemble.c", }; diff --git a/docs/PRD-msa-consistency.md b/docs/PRD-msa-consistency.md new file mode 100644 index 0000000..aaef8dd --- /dev/null +++ b/docs/PRD-msa-consistency.md @@ -0,0 +1,98 @@ +# PRD: MSA-Derived Consistency Merge for Ensemble Alignment + +## Goal + +Add an alternative ensemble merge strategy that extracts residue-residue +consistency scores from N completed ensemble MSAs and uses them as bonus +weights in a final progressive alignment, replacing the POAR consensus path. + +## Background + +The current ensemble pipeline: +1. Runs N progressive alignments with diverse parameters +2. Builds a POAR table, scores each run, picks the best +3. Either selects the best single run or builds a POAR consensus MSA + +This works well for F1 (0.810) but TC lags (0.523 vs 0.58+ for MUSCLE/MAFFT). +TC measures column-level consistency — exactly what a consistency-transformed +re-alignment should improve. + +The idea: instead of POAR consensus, extract pairwise residue correspondences +from the N ensemble MSAs (which residues land in the same column across +multiple runs?) and use those agreement counts as bonus scores during a fresh +progressive alignment. The best-scoring run's gap penalties and matrix are +reused for the final alignment — no extra parameters to optimize. + +## Design + +### Reuse `consistency_table` + `sparse_bonus` + +The existing anchor consistency system provides: +- `consistency_table` — stores position maps indexed by [seq × K + slot] +- `sparse_bonus` — sparse bonus matrix queried during DP +- `anchor_consistency_get_bonus()` / `_get_bonus_profile()` — computes + bonuses for seq-seq, seq-profile, and profile-profile merges +- DP integration — `sparse_bonus_lookup()` already wired into match scores + +For MSA consistency, K = n_runs, and position maps come from MSA column +structure instead of pairwise alignments. The "anchor position" is replaced +by "column index" — the `get_bonus` functions work unchanged. + +### Data flow (when `consistency_merge = 1`) + +``` +1. Run N alignments with diverse params [unchanged] +2. Extract POARs from each alignment [unchanged] +3. Score alignments, find best_k [unchanged] +4. NEW: Build consistency_table from N aligned MSAs +5. NEW: Copy original MSA, attach consistency_table +6. NEW: Run progressive alignment with best_k's params + consistency bonus +7. Copy result back to original MSA +8. Compute per-residue confidence from POAR [unchanged] +``` + +### Column map extraction + +For each ensemble MSA k, for each sequence i, build: +``` +col_map[pos] = column index in aligned MSA k +``` +where `pos` is the ungapped residue position. Stored in +`consistency_table.pos_maps[seq_i * K + run_k]`. + +When queried for pair (seq_a, seq_b): if both map to the same column in +run k, that's a vote. Bonus = weight × (votes / K). + +## API Changes + +### C: `kalign_ensemble_config` +- `int consistency_merge;` — 0 = POAR (default), 1 = MSA consistency +- `float consistency_merge_weight;` — bonus weight (default 2.0) + +### Python: `ensemble_custom_file_to_file()` +- `consistency_merge` (int, default 0) +- `consistency_merge_weight` (float, default 2.0) + +### Optimizer: `optimize_unified.py` +- `consistency_merge`: Choice({0, 1}), only when n_runs > 1 +- `consistency_merge_weight`: Real([0.5, 10.0]), only when consistency_merge=1 + +## Weight Parameter + +A weight parameter is needed. The bonus is added to the substitution score +in the DP. Too high → rigidly reproduces ensemble consensus. Too low → no +benefit. The optimizer finds the sweet spot. Default 2.0. + +## Implementation + +### New files +- `lib/src/msa_consistency.c` — `msa_consistency_build()` +- `lib/src/msa_consistency.h` — header + +### Modified files +- `lib/include/kalign/kalign_config.h` — ensemble_config fields +- `lib/src/aln_wrap.c` — defaults function +- `lib/src/ensemble.c` — consistency merge path +- `lib/CMakeLists.txt` — new source file +- `python-kalign/_core.cpp` — expose params +- `benchmarks/optimize_unified.py` — optimizer variables diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 4292510..1d9488f 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -54,6 +54,7 @@ set(source_files src/poar.c src/consensus_msa.c src/anchor_consistency.c + src/msa_consistency.c src/ensemble.c # src/coretralign.c diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 723ee18..7385ad2 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -26,6 +26,9 @@ #define KALIGN_MATRIX_DNA 5 /* DNA match/mismatch (+5/-4) */ #define KALIGN_MATRIX_DNA_INTERNAL 6 /* DNA internal (tgpe=8) */ #define KALIGN_MATRIX_RNA 7 /* RNA RIBOSUM-like (~160-383) */ +#define KALIGN_MATRIX_NUC_1PAM 10 /* Kimura 1PAM, kappa=2 (close) */ +#define KALIGN_MATRIX_NUC_20PAM 11 /* Kimura 20PAM, kappa=2 (moderate)*/ +#define KALIGN_MATRIX_NUC_200PAM 12 /* Kimura 200PAM, kappa=2 (distant)*/ /* Backward compatibility — old KALIGN_TYPE_* map to KALIGN_MATRIX_*. KALIGN_TYPE_PROTEIN_DIVERGENT stays at 4 (GONNET, dead code). */ diff --git a/lib/include/kalign/kalign_config.h b/lib/include/kalign/kalign_config.h index 4924bdd..21fc9e2 100644 --- a/lib/include/kalign/kalign_config.h +++ b/lib/include/kalign/kalign_config.h @@ -29,6 +29,8 @@ struct kalign_run_config { Only used when n_runs > 1. */ struct kalign_ensemble_config { int min_support; /* POAR consensus threshold (0 = auto) */ + int consistency_merge; /* 0 = POAR (default), 1 = MSA consistency merge */ + float consistency_merge_weight; /* MSA consistency bonus weight (default: 2.0)*/ }; #endif diff --git a/lib/src/aln_param.c b/lib/src/aln_param.c index e3b9871..c219e67 100644 --- a/lib/src/aln_param.c +++ b/lib/src/aln_param.c @@ -13,6 +13,8 @@ static int set_subm_gaps_PFASUM60(struct aln_param *ap); static int set_subm_gaps_DNA(struct aln_param *ap); static int set_subm_gaps_DNA_internal(struct aln_param *ap); static int set_subm_gaps_RNA(struct aln_param *ap); +static int set_subm_gaps_nuc_kimura(struct aln_param *ap, float match, + float transition, float transversion); int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int type, float gpo, float gpe, float tgpe) { @@ -43,6 +45,15 @@ int aln_param_init(struct aln_param **aln_param,int biotype , int n_threads, int case KALIGN_MATRIX_RNA: set_subm_gaps_RNA(ap); break; + case KALIGN_MATRIX_NUC_1PAM: + set_subm_gaps_nuc_kimura(ap, 5.0f, -14.2f, -16.8f); + break; + case KALIGN_MATRIX_NUC_20PAM: + set_subm_gaps_nuc_kimura(ap, 5.0f, -4.6f, -7.2f); + break; + case KALIGN_MATRIX_NUC_200PAM: + set_subm_gaps_nuc_kimura(ap, 5.0f, 0.8f, -3.4f); + break; case KALIGN_MATRIX_PFASUM43: case KALIGN_MATRIX_PFASUM60: case KALIGN_MATRIX_CORBLOSUM66: @@ -360,6 +371,33 @@ int set_subm_gaps_RNA(struct aln_param* ap) return OK; } +/* Kimura two-parameter nucleotide matrix. + Alphabet: A=0, C=1, G=2, T/U=3, N=4. + Transitions: A<->G, C<->T. Transversions: all others. + Gap penalties use the same defaults as the DNA matrix. */ +int set_subm_gaps_nuc_kimura(struct aln_param *ap, float match, + float transition, float transversion) +{ + int i, j; + /* Transition pairs: (0,2)=A-G, (2,0)=G-A, (1,3)=C-T, (3,1)=T-C */ + for(i = 0; i < 5; i++){ + for(j = 0; j < 5; j++){ + if(i == j){ + ap->subm[i][j] = match; + }else if((i == 0 && j == 2) || (i == 2 && j == 0) || + (i == 1 && j == 3) || (i == 3 && j == 1)){ + ap->subm[i][j] = transition; + }else{ + ap->subm[i][j] = transversion; + } + } + } + ap->gpo = 8.0f; + ap->gpe = 6.0f; + ap->tgpe = 0.0f; + return OK; +} + void aln_param_free(struct aln_param *ap) { if(ap){ diff --git a/lib/src/aln_run.c b/lib/src/aln_run.c index 8744886..f55a218 100644 --- a/lib/src/aln_run.c +++ b/lib/src/aln_run.c @@ -20,6 +20,7 @@ /* #include "weave_alignment.h" */ #include "sp_score.h" #include "anchor_consistency.h" +#include "msa_consistency.h" #include #include @@ -261,9 +262,11 @@ int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id) /* Compute consistency bonus for all merge types */ { - struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; - if(ct != NULL){ - int dp_row_node, dp_col_node, dp_rows, dp_cols; + int dp_row_node, dp_col_node, dp_rows, dp_cols; + int have_consistency = (msa->poar_consistency != NULL) + || (msa->consistency_table != NULL); + + if(have_consistency){ if(msa->nsip[a] == 1 && msa->nsip[b] == 1){ if(m->len_a < m->len_b){ dp_row_node = a; dp_rows = m->len_a; @@ -287,9 +290,18 @@ int do_align(struct msa* msa,struct aln_tasks* t,struct aln_mem* m, int task_id) dp_col_node = a; dp_cols = m->len_a; } } - RUN(anchor_consistency_get_bonus_profile(ct, msa, - dp_row_node, dp_rows, dp_col_node, dp_cols, - &m->consistency)); + + if(msa->poar_consistency != NULL){ + RUN(poar_consistency_get_bonus( + (struct poar_consistency_ctx*)msa->poar_consistency, + msa, dp_row_node, dp_rows, dp_col_node, dp_cols, + &m->consistency)); + }else{ + RUN(anchor_consistency_get_bonus_profile( + (struct consistency_table*)msa->consistency_table, + msa, dp_row_node, dp_rows, dp_col_node, dp_cols, + &m->consistency)); + } } } @@ -563,9 +575,11 @@ int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, /* Compute consistency bonus for all merge types */ m->consistency = NULL; { - struct consistency_table* ct = (struct consistency_table*)msa->consistency_table; - if(ct != NULL){ - int dp_row_node, dp_col_node, dp_rows, dp_cols; + int dp_row_node, dp_col_node, dp_rows, dp_cols; + int have_consistency = (msa->poar_consistency != NULL) + || (msa->consistency_table != NULL); + + if(have_consistency){ if(msa->nsip[a] == 1 && msa->nsip[b] == 1){ if(len_a < len_b){ dp_row_node = a; dp_rows = len_a; @@ -589,9 +603,18 @@ int do_align_inline_refine(struct msa* msa, struct aln_tasks* t, dp_col_node = a; dp_cols = len_a; } } - RUN(anchor_consistency_get_bonus_profile(ct, msa, - dp_row_node, dp_rows, dp_col_node, dp_cols, - &m->consistency)); + + if(msa->poar_consistency != NULL){ + RUN(poar_consistency_get_bonus( + (struct poar_consistency_ctx*)msa->poar_consistency, + msa, dp_row_node, dp_rows, dp_col_node, dp_cols, + &m->consistency)); + }else{ + RUN(anchor_consistency_get_bonus_profile( + (struct consistency_table*)msa->consistency_table, + msa, dp_row_node, dp_rows, dp_col_node, dp_cols, + &m->consistency)); + } } } diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index 6283ab5..848e30d 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -707,6 +707,8 @@ struct kalign_ensemble_config kalign_ensemble_config_defaults(void) { struct kalign_ensemble_config ens; ens.min_support = 0; + ens.consistency_merge = 0; + ens.consistency_merge_weight = 2.0f; return ens; } @@ -775,77 +777,103 @@ static void preset_run(struct kalign_run_config *r, r->tree_noise = noise; } -/* ---- Protein presets (NSGA-III optimized on BAliBASE v4) ---- */ +/* Helper: set shared consistency params on all runs */ +static void preset_consistency(struct kalign_run_config *runs, int n_runs, + int anchors, float weight) +{ + for(int i = 0; i < n_runs; i++){ + runs[i].consistency_anchors = anchors; + runs[i].consistency_weight = weight; + } +} + +/* ---- Protein presets (NSGA-III optimized on BAliBASE v4, gen 41) ---- */ static int preset_protein(const char *m, struct kalign_run_config *runs, int *n_runs, struct kalign_ensemble_config *ens) { + /* fast: single run, ~34s on BAliBASE. F1=0.737 R=0.818 P=0.670 */ if(strcasecmp(m, "fast") == 0){ *n_runs = 1; preset_run(&runs[0], KALIGN_MATRIX_PFASUM60, - 8.4087f, 0.5153f, 0.4927f, - 1.448f, 1.063f, + 8.626f, 0.843f, 0.433f, + 0.592f, 1.534f, 0, KALIGN_REFINE_NONE, - 42, 0.1623f); + 0, 0.0f); return 0; } + /* default: single run with inline refinement, ~130s. + F1=0.750 R=0.779 P=0.724 */ if(strcasecmp(m, "default") == 0){ + *n_runs = 1; + preset_run(&runs[0], + KALIGN_MATRIX_PFASUM60, + 8.121f, 0.684f, 0.560f, + 1.661f, 2.356f, + 0, KALIGN_REFINE_INLINE, + 0, 0.0f); + preset_consistency(runs, 1, 1, 1.640f); + return 0; + } + + /* recall: 5-run ensemble optimized for recall, ~1175s. + F1=0.777 R=0.837 P=0.726 */ + if(strcasecmp(m, "recall") == 0){ *n_runs = 5; - float vsm = 1.885f; - float sw = 0.592f; - int ra = 0; - int ref = KALIGN_REFINE_NONE; - - /* BUG FIX: these were labeled "gonnet" in the optimizer - but actually ran PFASUM43 (optimizer's matrix_map_int - mapped index 2 → KALIGN_TYPE_PROTEIN = PFASUM43). */ - preset_run(&runs[0], KALIGN_MATRIX_PFASUM43, - 9.5703f, 0.6206f, 1.5751f, - vsm, sw, ra, ref, 42, 0.063f); - preset_run(&runs[1], KALIGN_MATRIX_PFASUM43, - 5.6154f, 0.5469f, 1.0163f, - vsm, sw, ra, ref, 43, 0.2828f); - preset_run(&runs[2], KALIGN_MATRIX_PFASUM43, - 4.8979f, 1.3657f, 1.2367f, - vsm, sw, ra, ref, 44, 0.4046f); + preset_run(&runs[0], KALIGN_MATRIX_CORBLOSUM66, + 5.416f, 1.071f, 1.620f, + 2.536f, 0.282f, + 1, KALIGN_REFINE_ALL, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_CORBLOSUM66, + 12.091f, 1.024f, 1.284f, + 2.078f, 0.282f, + 1, KALIGN_REFINE_INLINE, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_PFASUM60, + 6.970f, 2.919f, 0.632f, + 0.877f, 0.282f, + 1, KALIGN_REFINE_NONE, 44, 0.0f); preset_run(&runs[3], KALIGN_MATRIX_PFASUM43, - 7.244f, 0.9013f, 0.7332f, - vsm, sw, ra, ref, 45, 0.3067f); + 6.173f, 1.102f, 0.510f, + 1.276f, 0.282f, + 1, KALIGN_REFINE_ALL, 45, 0.0f); preset_run(&runs[4], KALIGN_MATRIX_PFASUM43, - 8.4354f, 1.8028f, 0.919f, - vsm, sw, ra, ref, 46, 0.1964f); - - ens->min_support = 3; + 5.278f, 1.764f, 1.088f, + 2.315f, 0.282f, + 1, KALIGN_REFINE_CONFIDENT, 46, 0.0f); + preset_consistency(runs, 5, 3, 2.120f); + ens->min_support = 0; return 0; } + /* accurate: 5-run ensemble optimized for F1, ~2005s. + F1=0.814 R=0.782 P=0.848 */ if(strcasecmp(m, "accurate") == 0){ *n_runs = 5; - float vsm = 1.682f; - float sw = 1.48f; - int ra = 2; - int ref = KALIGN_REFINE_NONE; - preset_run(&runs[0], KALIGN_MATRIX_PFASUM43, - 13.1073f, 0.6667f, 0.613f, - vsm, sw, ra, ref, 42, 0.3472f); + 8.682f, 0.650f, 1.465f, + 2.532f, 1.468f, + 1, KALIGN_REFINE_INLINE, 42, 0.0f); preset_run(&runs[1], KALIGN_MATRIX_PFASUM43, - 7.3036f, 0.6285f, 2.8521f, - vsm, sw, ra, ref, 43, 0.2264f); - preset_run(&runs[2], KALIGN_MATRIX_PFASUM43, - 2.2452f, 2.0447f, 0.5878f, - vsm, sw, ra, ref, 44, 0.1481f); - preset_run(&runs[3], KALIGN_MATRIX_PFASUM43, - 3.9617f, 0.8429f, 0.5156f, - vsm, sw, ra, ref, 45, 0.4338f); + 6.148f, 1.174f, 1.297f, + 2.011f, 1.468f, + 1, KALIGN_REFINE_ALL, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_CORBLOSUM66, + 3.622f, 1.004f, 0.631f, + 0.872f, 1.468f, + 1, KALIGN_REFINE_CONFIDENT, 44, 0.0f); + preset_run(&runs[3], KALIGN_MATRIX_CORBLOSUM66, + 5.988f, 0.552f, 0.495f, + 1.914f, 1.468f, + 1, KALIGN_REFINE_CONFIDENT, 45, 0.0f); preset_run(&runs[4], KALIGN_MATRIX_PFASUM43, - 7.5402f, 1.8516f, 0.8772f, - vsm, sw, ra, ref, 46, 0.1979f); - + 13.939f, 2.629f, 1.406f, + 1.830f, 1.468f, + 1, KALIGN_REFINE_NONE, 46, 0.0f); + preset_consistency(runs, 5, 8, 2.457f); ens->min_support = 3; return 0; } @@ -853,63 +881,191 @@ static int preset_protein(const char *m, return -1; } -/* ---- DNA/RNA preset stubs (use matrix defaults, to be optimized) ---- */ +/* ---- RNA presets (NSGA-III optimized on BRAliBASE, gen 88) ---- + * + * NOTE: fast ≈ recall (both single-run K200, nearly identical params). + * RNA alignments are "easy" enough that a single run captures + * most of the quality. Kept separate for future tuning. + */ -static int preset_dna(const char *m, +static int preset_rna(const char *m, struct kalign_run_config *runs, int *n_runs, struct kalign_ensemble_config *ens) { - /* All DNA modes use a single run with standard DNA matrix defaults. - To be replaced with optimized presets after benchmarking. */ - (void)ens; - *n_runs = 1; - runs[0] = kalign_run_config_defaults(); - runs[0].matrix = KALIGN_MATRIX_DNA; - runs[0].gpo = 8.0f; - runs[0].gpe = 6.0f; - runs[0].tgpe = 0.0f; - runs[0].vsm_amax = 0.0f; - runs[0].seq_weights = 0.0f; - + /* fast: single run, ~5s on BRAliBASE. F1=0.828 R=0.832 P=0.825 */ if(strcasecmp(m, "fast") == 0){ + *n_runs = 1; + preset_run(&runs[0], + KALIGN_MATRIX_NUC_200PAM, + 11.939f, 1.598f, 2.890f, + 0.636f, 1.363f, + 0, KALIGN_REFINE_INLINE, + 0, 0.0f); + preset_consistency(runs, 1, 3, 2.372f); return 0; } + + /* default: 3-run ensemble, ~8s. F1=0.835 R=0.804 P=0.869 */ if(strcasecmp(m, "default") == 0){ + *n_runs = 3; + preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, + 19.633f, 0.582f, 2.974f, + 1.929f, 1.569f, + 0, KALIGN_REFINE_NONE, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, + 13.155f, 1.631f, 2.717f, + 0.733f, 1.569f, + 0, KALIGN_REFINE_NONE, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, + 18.052f, 0.391f, 2.316f, + 0.598f, 1.569f, + 0, KALIGN_REFINE_NONE, 44, 0.0f); + preset_consistency(runs, 3, 1, 2.636f); + ens->min_support = 2; + return 0; + } + + /* recall: single run, ~6s. F1=0.829 R=0.833 P=0.826 */ + if(strcasecmp(m, "recall") == 0){ + *n_runs = 1; + preset_run(&runs[0], + KALIGN_MATRIX_NUC_200PAM, + 11.939f, 1.591f, 2.890f, + 0.065f, 1.363f, + 0, KALIGN_REFINE_CONFIDENT, + 0, 0.0f); + preset_consistency(runs, 1, 3, 2.572f); return 0; } + + /* accurate: 3-run ensemble with realign, ~26s. + F1=0.836 R=0.811 P=0.863 */ if(strcasecmp(m, "accurate") == 0){ + *n_runs = 3; + preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, + 10.799f, 1.389f, 2.718f, + 0.094f, 0.364f, + 2, KALIGN_REFINE_CONFIDENT, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, + 15.595f, 1.932f, 2.486f, + 0.190f, 0.364f, + 2, KALIGN_REFINE_ALL, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, + 14.749f, 1.041f, 2.450f, + 2.712f, 0.364f, + 2, KALIGN_REFINE_CONFIDENT, 44, 0.0f); + preset_consistency(runs, 3, 3, 2.634f); + ens->min_support = 2; return 0; } + return -1; } -static int preset_rna(const char *m, +/* ---- DNA presets (NSGA-III optimized on MDSA, gen 100) ---- + * + * NOTE: default = accurate (optimizer converged on same solution). + * DNA alignments are harder than RNA; even "fast" needs ens3+realign. + * Kept separate for future tuning. + */ + +static int preset_dna(const char *m, struct kalign_run_config *runs, int *n_runs, struct kalign_ensemble_config *ens) { - /* All RNA modes use a single run with standard RNA matrix defaults. - To be replaced with optimized presets after benchmarking. */ - (void)ens; - *n_runs = 1; - runs[0] = kalign_run_config_defaults(); - runs[0].matrix = KALIGN_MATRIX_RNA; - runs[0].gpo = 217.0f; - runs[0].gpe = 39.4f; - runs[0].tgpe = 292.6f; - runs[0].vsm_amax = 0.0f; - runs[0].seq_weights = 0.0f; - + /* fast: 3-run ensemble with realign, ~18s on MDSA. + F1=0.764 R=0.741 P=0.788 */ if(strcasecmp(m, "fast") == 0){ + *n_runs = 3; + preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, + 19.660f, 0.600f, 2.690f, + 1.050f, 2.710f, + 1, KALIGN_REFINE_NONE, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, + 19.470f, 1.600f, 2.490f, + 0.140f, 2.710f, + 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, + 17.200f, 3.810f, 2.770f, + 0.070f, 2.710f, + 1, KALIGN_REFINE_NONE, 44, 0.0f); + ens->min_support = 2; return 0; } + + /* default: 3-run ensemble with realign, ~35s. + F1=0.775 R=0.737 P=0.816 */ if(strcasecmp(m, "default") == 0){ + *n_runs = 3; + preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, + 19.350f, 0.310f, 2.860f, + 1.450f, 1.830f, + 1, KALIGN_REFINE_INLINE, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, + 19.020f, 1.980f, 2.510f, + 0.490f, 1.830f, + 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, + 17.850f, 3.000f, 2.450f, + 1.380f, 1.830f, + 1, KALIGN_REFINE_NONE, 44, 0.0f); + preset_consistency(runs, 3, 1, 0.780f); + ens->min_support = 2; + return 0; + } + + /* recall: 5-run ensemble with realign, ~65s. + F1=0.765 R=0.760 P=0.770 */ + if(strcasecmp(m, "recall") == 0){ + *n_runs = 5; + preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, + 19.370f, 0.660f, 2.650f, + 2.470f, 1.820f, + 1, KALIGN_REFINE_INLINE, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, + 19.110f, 1.580f, 2.490f, + 1.060f, 1.820f, + 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_200PAM, + 16.730f, 1.190f, 2.480f, + 0.360f, 1.820f, + 1, KALIGN_REFINE_CONFIDENT, 44, 0.0f); + preset_run(&runs[3], KALIGN_MATRIX_NUC_200PAM, + 16.730f, 4.090f, 2.200f, + 0.890f, 1.820f, + 1, KALIGN_REFINE_NONE, 45, 0.0f); + preset_run(&runs[4], KALIGN_MATRIX_NUC_200PAM, + 9.340f, 1.380f, 1.100f, + 1.400f, 1.820f, + 1, KALIGN_REFINE_CONFIDENT, 46, 0.0f); + preset_consistency(runs, 5, 1, 0.630f); + ens->min_support = 2; return 0; } + + /* accurate: same as default (optimizer converged on same solution). + F1=0.775 R=0.737 P=0.816 */ if(strcasecmp(m, "accurate") == 0){ + *n_runs = 3; + preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, + 19.350f, 0.310f, 2.860f, + 1.450f, 1.830f, + 1, KALIGN_REFINE_INLINE, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, + 19.020f, 1.980f, 2.510f, + 0.490f, 1.830f, + 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, + 17.850f, 3.000f, 2.450f, + 1.380f, 1.830f, + 1, KALIGN_REFINE_NONE, 44, 0.0f); + preset_consistency(runs, 3, 1, 0.780f); + ens->min_support = 2; return 0; } + return -1; } @@ -924,11 +1080,11 @@ int kalign_get_mode_preset(const char *mode, *ens = kalign_ensemble_config_defaults(); if(biotype == ALN_BIOTYPE_DNA){ - /* Detect RNA from matrix type if needed. - For now, DNA biotype covers both DNA and RNA. - Use RNA presets when RNA matrix was explicitly requested, - otherwise fall back to DNA presets. */ - return preset_dna(m, runs, n_runs, ens); + /* TODO: distinguish DNA vs RNA for better dispatch. + Currently defaults to RNA presets. DNA presets are + available via preset_dna() but need RNA detection + (e.g. presence of U) to select automatically. */ + return preset_rna(m, runs, n_runs, ens); } /* Default: protein presets */ diff --git a/lib/src/ensemble.c b/lib/src/ensemble.c index bc70245..cf06362 100644 --- a/lib/src/ensemble.c +++ b/lib/src/ensemble.c @@ -11,6 +11,8 @@ #include "aln_wrap.h" #include "poar.h" #include "consensus_msa.h" +#include "anchor_consistency.h" +#include "msa_consistency.h" #include "kalign/kalign.h" #include "kalign/kalign_config.h" @@ -894,103 +896,154 @@ int kalign_ensemble_from_configs(struct msa* msa, /* POAR save removed from ensemble_config — debug feature */ - /* Determine min_support */ + /* Determine merge strategy */ int min_support = (ens != NULL) ? ens->min_support : 0; + int use_consistency_merge = (ens != NULL) ? ens->consistency_merge : 0; + + if(use_consistency_merge){ + /* ---- POAR consistency merge path ---- + * Use the already-built POAR table as a source of pairwise + * residue consistency scores for a fresh progressive alignment. + * Uses best_k's gap penalties and matrix. */ + float cm_weight = (ens != NULL) ? ens->consistency_merge_weight : 2.0f; + struct poar_consistency_ctx poar_ctx; + poar_ctx.poar = poar; + poar_ctx.n_runs = n_runs; + poar_ctx.weight = cm_weight; - if(min_support > 0){ - RUN(build_consensus_from_poar(poar, msa, numseq, min_support, - &consensus_msa)); - use_consensus = 1; - if(!msa->quiet){ - LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); - } - }else{ - double consensus_score = 0.0; - int min_sup = (n_runs + 2) / 3; - if(min_sup < 2) min_sup = 2; + copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = msa->quiet ? 1 : 0; - RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, - &consensus_msa)); - - RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, - &consensus_score)); + /* Attach POAR consistency context — the progressive alignment + will pick it up via msa->poar_consistency in aln_run.c */ + copy->poar_consistency = &poar_ctx; if(!msa->quiet){ - LOG_MSG(" Consensus score: %.1f (selection: %.1f)", - consensus_score, scores[best_k]); + LOG_MSG(" Consistency merge (weight=%.1f) using run %d params", + cm_weight, best_k + 1); } - if(consensus_score > scores[best_k]){ + /* Run a fresh progressive alignment with best_k's params. + No additional anchor consistency or realign — the POAR + consistency signal is the main guide. */ + RUN(kalign_run_seeded(copy, n_threads, runs[best_k].matrix, + runs[best_k].gpo, runs[best_k].gpe, + runs[best_k].tgpe, + KALIGN_REFINE_NONE, 0, + 0, 0.0f, /* deterministic tree */ + runs[best_k].dist_scale, + runs[best_k].vsm_amax, + 0.0f, /* no seq_weights in ensemble */ + 0, 0.0f /* no anchor consistency */)); + + /* Clear the non-owning pointer before freeing the copy */ + copy->poar_consistency = NULL; + + RUN(copy_alignment_to_msa(msa, copy, numseq)); + kalign_free_msa(copy); + copy = NULL; + + }else{ + /* ---- POAR consensus / selection path (existing) ---- */ + + if(min_support > 0){ + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa)); use_consensus = 1; if(!msa->quiet){ - LOG_MSG(" Using consensus alignment"); + LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); } }else{ - kalign_free_msa(consensus_msa); - consensus_msa = NULL; - if(!msa->quiet){ - LOG_MSG(" Keeping selection winner"); - } - } - } + double consensus_score = 0.0; + int min_sup = (n_runs + 2) / 3; + if(min_sup < 2) min_sup = 2; - /* Post-selection refinement: re-run the winner with REFINE_CONFIDENT */ - if(!use_consensus){ - copy = NULL; - RUN(msa_cpy(©, msa)); - copy->quiet = 1; + RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, + &consensus_msa)); - if(!msa->quiet){ - LOG_MSG(" Refining run %d...", best_k + 1); - } + RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, + &consensus_score)); - /* Post-selection refinement always uses REFINE_CONFIDENT and - the winning run's parameters (matching old behavior). */ - RUN(kalign_run_seeded(copy, n_threads, runs[best_k].matrix, - runs[best_k].gpo, runs[best_k].gpe, - runs[best_k].tgpe, - KALIGN_REFINE_CONFIDENT, 0, - runs[best_k].tree_seed, runs[best_k].tree_noise, - runs[best_k].dist_scale, runs[best_k].vsm_amax, - runs[best_k].seq_weights, - runs[best_k].consistency_anchors, - runs[best_k].consistency_weight)); - - double refined_score = 0.0; - RUN(score_single_msa(copy, poar, numseq, n_runs, - &refined_score)); + if(!msa->quiet){ + LOG_MSG(" Consensus score: %.1f (selection: %.1f)", + consensus_score, scores[best_k]); + } - if(!msa->quiet){ - LOG_MSG(" Refined score: %.1f (was %.1f)", - refined_score, scores[best_k]); + if(consensus_score > scores[best_k]){ + use_consensus = 1; + if(!msa->quiet){ + LOG_MSG(" Using consensus alignment"); + } + }else{ + kalign_free_msa(consensus_msa); + consensus_msa = NULL; + if(!msa->quiet){ + LOG_MSG(" Keeping selection winner"); + } + } } - if(refined_score > scores[best_k]){ - kalign_free_msa(alignments[best_k]); - alignments[best_k] = copy; + /* Post-selection refinement: re-run the winner with REFINE_CONFIDENT */ + if(!use_consensus){ copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = 1; + if(!msa->quiet){ - LOG_MSG(" Using refined alignment"); + LOG_MSG(" Refining run %d...", best_k + 1); } - }else{ - kalign_free_msa(copy); - copy = NULL; + + RUN(kalign_run_seeded(copy, n_threads, runs[best_k].matrix, + runs[best_k].gpo, runs[best_k].gpe, + runs[best_k].tgpe, + KALIGN_REFINE_CONFIDENT, 0, + runs[best_k].tree_seed, runs[best_k].tree_noise, + runs[best_k].dist_scale, runs[best_k].vsm_amax, + runs[best_k].seq_weights, + runs[best_k].consistency_anchors, + runs[best_k].consistency_weight)); + + double refined_score = 0.0; + RUN(score_single_msa(copy, poar, numseq, n_runs, + &refined_score)); + if(!msa->quiet){ - LOG_MSG(" Keeping original alignment"); + LOG_MSG(" Refined score: %.1f (was %.1f)", + refined_score, scores[best_k]); + } + + if(refined_score > scores[best_k]){ + kalign_free_msa(alignments[best_k]); + alignments[best_k] = copy; + copy = NULL; + if(!msa->quiet){ + LOG_MSG(" Using refined alignment"); + } + }else{ + kalign_free_msa(copy); + copy = NULL; + if(!msa->quiet){ + LOG_MSG(" Keeping original alignment"); + } } } - } - MFREE(scores); - scores = NULL; + MFREE(scores); + scores = NULL; - /* Copy the winning alignment back into the original MSA */ - if(use_consensus){ - RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); - kalign_free_msa(consensus_msa); - consensus_msa = NULL; - }else{ - RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); + if(use_consensus){ + RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); + kalign_free_msa(consensus_msa); + consensus_msa = NULL; + }else{ + RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); + } + } + + if(scores){ + MFREE(scores); + scores = NULL; } RUN(compute_residue_confidence(poar, msa)); diff --git a/lib/src/msa_alloc.c b/lib/src/msa_alloc.c index bde353c..b86354b 100644 --- a/lib/src/msa_alloc.c +++ b/lib/src/msa_alloc.c @@ -27,6 +27,7 @@ int alloc_msa(struct msa** msa, int numseq) m->sip = NULL; m->nsip = NULL; m->consistency_table = NULL; + m->poar_consistency = NULL; MMALLOC(m->sequences, sizeof(struct msa_seq*) * m->alloc_numseq); diff --git a/lib/src/msa_consistency.c b/lib/src/msa_consistency.c new file mode 100644 index 0000000..7481bbe --- /dev/null +++ b/lib/src/msa_consistency.c @@ -0,0 +1,185 @@ +#include "tldevel.h" + +#include "msa_struct.h" +#include "anchor_consistency.h" /* for sparse_bonus, sparse_bonus_free */ +#include "poar.h" + +#define MSA_CONSISTENCY_IMPORT +#include "msa_consistency.h" + +/* Flat index for pair (i,j) where i < j. + Duplicated from poar.c (static there). */ +static inline int poar_pair_idx(int i, int j, int numseq) +{ + return i * numseq - (i * (i + 1)) / 2 + (j - i - 1); +} + +/* Build ungapped_pos -> DP_position map for a member sequence. + * Walks gaps[] to find the DP column for each ungapped residue. + * Caller must free the returned array. */ +static int build_ungapped_to_dp(struct msa* msa, int seq_idx, + int** map_out, int* ungapped_len_out) +{ + int* gaps = msa->sequences[seq_idx]->gaps; + int seq_len = msa->sequences[seq_idx]->len; + int* map = NULL; + int dp, p, g; + + if(seq_len == 0){ + *map_out = NULL; + *ungapped_len_out = 0; + return OK; + } + + MMALLOC(map, sizeof(int) * seq_len); + + dp = 0; + for(p = 0; p <= seq_len; p++){ + for(g = 0; g < gaps[p]; g++){ + dp++; + } + if(p < seq_len){ + map[p] = dp; + dp++; + } + } + + *map_out = map; + *ungapped_len_out = seq_len; + return OK; +ERROR: + if(map) MFREE(map); + *map_out = NULL; + *ungapped_len_out = 0; + return FAIL; +} + +#define POAR_BONUS_K 16 /* max distinct target positions per DP row */ + +int poar_consistency_get_bonus(struct poar_consistency_ctx* ctx, + struct msa* msa, + int node_a, int len_a, + int node_b, int len_b, + struct sparse_bonus** bonus_out) +{ + struct sparse_bonus* sb = NULL; + int* map_a = NULL; + int* map_b = NULL; + int K = POAR_BONUS_K; + int numseq = ctx->poar->numseq; + int n_members_a = msa->nsip[node_a]; + int n_members_b = msa->nsip[node_b]; + int* members_a = msa->sip[node_a]; + int* members_b = msa->sip[node_b]; + float denom = (float)n_members_a * (float)n_members_b * (float)ctx->n_runs; + float pair_weight = ctx->weight / denom; + int ma, mb, e, i; + + /* Allocate sparse bonus */ + MMALLOC(sb, sizeof(struct sparse_bonus)); + sb->cols = NULL; + sb->vals = NULL; + sb->n_rows = len_a; + sb->K = K; + + MMALLOC(sb->cols, sizeof(int) * len_a * K); + MMALLOC(sb->vals, sizeof(float) * len_a * K); + for(i = 0; i < len_a * K; i++){ + sb->cols[i] = -1; + sb->vals[i] = 0.0f; + } + + /* For each pair of member sequences, look up POAR entries */ + for(ma = 0; ma < n_members_a; ma++){ + int sa = members_a[ma]; + int sa_len = 0; + + RUN(build_ungapped_to_dp(msa, sa, &map_a, &sa_len)); + + for(mb = 0; mb < n_members_b; mb++){ + int sb_seq = members_b[mb]; + int sb_len = 0; + int lo, hi; + int pidx; + struct poar_pair* pp; + int swapped; + + if(sa == sb_seq) continue; + + RUN(build_ungapped_to_dp(msa, sb_seq, &map_b, &sb_len)); + + /* POAR pairs are stored with lo < hi */ + if(sa < sb_seq){ + lo = sa; hi = sb_seq; + swapped = 0; + }else{ + lo = sb_seq; hi = sa; + swapped = 1; + } + + pidx = poar_pair_idx(lo, hi, numseq); + pp = ctx->poar->pairs[pidx]; + + if(pp != NULL){ + for(e = 0; e < pp->n_entries; e++){ + uint32_t key = pp->entries[e].key; + uint32_t support = pp->entries[e].support; + int pos_lo = (int)(key >> 20); + int pos_hi = (int)(key & 0xFFFFF); + int pos_sa, pos_sb; + int dp_row, dp_col; + int count, base, slot, s; + float val; + + if(swapped){ + pos_sa = pos_hi; + pos_sb = pos_lo; + }else{ + pos_sa = pos_lo; + pos_sb = pos_hi; + } + + if(pos_sa >= sa_len || pos_sb >= sb_len) continue; + if(map_a == NULL || map_b == NULL) continue; + + dp_row = map_a[pos_sa]; + dp_col = map_b[pos_sb]; + + if(dp_row >= len_a || dp_col >= len_b) continue; + + count = __builtin_popcount(support); + val = pair_weight * (float)count; + + /* Insert into sparse bonus */ + base = dp_row * K; + slot = -1; + for(s = 0; s < K; s++){ + if(sb->cols[base + s] == dp_col){ + slot = s; break; + } + if(sb->cols[base + s] < 0){ + slot = s; break; + } + } + if(slot >= 0){ + sb->vals[base + slot] += val; + sb->cols[base + slot] = dp_col; + } + } + } + + if(map_b){ MFREE(map_b); map_b = NULL; } + } + + if(map_a){ MFREE(map_a); map_a = NULL; } + } + + *bonus_out = sb; + return OK; +ERROR: + sparse_bonus_free(sb); + if(map_a) MFREE(map_a); + if(map_b) MFREE(map_b); + *bonus_out = NULL; + return FAIL; +} diff --git a/lib/src/msa_consistency.h b/lib/src/msa_consistency.h new file mode 100644 index 0000000..2f4869e --- /dev/null +++ b/lib/src/msa_consistency.h @@ -0,0 +1,46 @@ +#ifndef MSA_CONSISTENCY_H +#define MSA_CONSISTENCY_H + +#ifdef MSA_CONSISTENCY_IMPORT +#define EXTERN +#else +#ifdef __cplusplus +#define EXTERN extern "C" +#else +#define EXTERN extern +#endif +#endif + +struct msa; +struct poar_table; +struct sparse_bonus; + +/* Context for POAR-based consistency scoring. + * Stored as a non-owning reference in msa->poar_consistency. */ +struct poar_consistency_ctx { + struct poar_table* poar; /* non-owning reference */ + int n_runs; + float weight; +}; + +/* Compute consistency bonus for a pair of tree nodes from POAR data. + * + * For each member sequence pair (sa in node_a, sb in node_b), looks up + * the POAR entries to find how often each residue pair was co-aligned + * across the N ensemble runs. Maps ungapped positions to DP positions + * using the current gap structure and accumulates bonuses into a + * sparse_bonus matrix. + * + * node_a/node_b: tree node indices (leaf = sequence index) + * len_a/len_b: DP dimensions (profile widths) + */ +EXTERN int poar_consistency_get_bonus(struct poar_consistency_ctx* ctx, + struct msa* msa, + int node_a, int len_a, + int node_b, int len_b, + struct sparse_bonus** bonus_out); + +#undef MSA_CONSISTENCY_IMPORT +#undef EXTERN + +#endif diff --git a/lib/src/msa_op.c b/lib/src/msa_op.c index 6f5fd2a..6b25289 100644 --- a/lib/src/msa_op.c +++ b/lib/src/msa_op.c @@ -458,6 +458,7 @@ int kalign_arr_to_msa(char** input_sequences, int* len, int numseq,struct msa** msa->seq_weights = NULL; msa->run_parallel = 0; msa->consistency_table = NULL; + msa->poar_consistency = NULL; msa->quiet = 1; MMALLOC(msa->sequences, sizeof(struct msa_seq*) * msa->alloc_numseq); diff --git a/lib/src/msa_struct.h b/lib/src/msa_struct.h index cd2869f..6dc3b20 100644 --- a/lib/src/msa_struct.h +++ b/lib/src/msa_struct.h @@ -50,6 +50,7 @@ struct msa{ uint8_t biotype; int quiet; void* consistency_table; /* struct consistency_table*, NULL when disabled */ + void* poar_consistency; /* struct poar_consistency_ctx*, NULL when disabled */ }; #undef MSA_STRUCT_IMPORT diff --git a/lib/src/pick_anchor.c b/lib/src/pick_anchor.c index 07e3092..bc5cde1 100644 --- a/lib/src/pick_anchor.c +++ b/lib/src/pick_anchor.c @@ -1,84 +1,125 @@ #include "tldevel.h" #include "msa_struct.h" +#include "bpm.h" -#define PICK_ANCHOR_IMPORT +#define PICK_ANCHOR_IMPORT #include "pick_anchor.h" - -struct sort_struct{ - int len; - int id; -}; - -int sort_by_len(const void *a, const void *b); - -int* select_seqs(struct msa* msa, int num_anchor); - -int* pick_anchor(struct msa* msa, int* n) +#define DEFAULT_NUM_ANCHORS 32 + +/* Farthest-first anchor selection. + * + * Picks K diverse anchor sequences by greedily selecting the sequence + * most distant (by BPM edit distance) from all already-selected anchors. + * This ensures the anchors span the full diversity of the dataset. + * + * Cost: K * N BPM calls — microseconds for typical inputs. + */ +static int* pick_anchor_farthest_first(struct msa* msa, int K, int* n_out) { + int numseq = msa->numseq; int* anchors = NULL; - int num_anchor = 0; + float* min_dist = NULL; /* min distance from each seq to any anchor */ + int i, k; + + if(K > numseq) K = numseq; + if(K < 1) K = 1; + + MMALLOC(anchors, sizeof(int) * K); + MMALLOC(min_dist, sizeof(float) * numseq); + + /* Pick first anchor: median-length sequence. + Sort would be overkill — just find the sequence closest + to the mean length. */ + { + float mean_len = 0.0f; + float best_diff = 1e30f; + int best_idx = 0; + for(i = 0; i < numseq; i++){ + mean_len += (float)msa->sequences[i]->len; + } + mean_len /= (float)numseq; + for(i = 0; i < numseq; i++){ + float diff = (float)msa->sequences[i]->len - mean_len; + if(diff < 0) diff = -diff; + if(diff < best_diff){ + best_diff = diff; + best_idx = i; + } + } + anchors[0] = best_idx; + } - ASSERT(msa != NULL, "No alignment."); + /* Initialize min_dist: BPM distance from each seq to first anchor */ + { + uint8_t* anchor_s = msa->sequences[anchors[0]]->s; + int anchor_len = msa->sequences[anchors[0]]->len; + for(i = 0; i < numseq; i++){ + uint8_t* si = msa->sequences[i]->s; + int li = msa->sequences[i]->len; + if(li > anchor_len){ + min_dist[i] = (float)BPM(si, anchor_s, li, anchor_len); + }else{ + min_dist[i] = (float)BPM(anchor_s, si, anchor_len, li); + } + } + min_dist[anchors[0]] = -1.0f; /* mark as selected */ + } + + /* Farthest-first: pick remaining K-1 anchors */ + for(k = 1; k < K; k++){ + /* Find the sequence with largest min_dist */ + float best_min = -1.0f; + int best_idx = 0; + for(i = 0; i < numseq; i++){ + if(min_dist[i] > best_min){ + best_min = min_dist[i]; + best_idx = i; + } + } + anchors[k] = best_idx; + min_dist[best_idx] = -1.0f; /* mark as selected */ + + /* Update min_dist with new anchor */ + uint8_t* anchor_s = msa->sequences[best_idx]->s; + int anchor_len = msa->sequences[best_idx]->len; + for(i = 0; i < numseq; i++){ + if(min_dist[i] < 0.0f) continue; /* already selected */ + uint8_t* si = msa->sequences[i]->s; + int li = msa->sequences[i]->len; + float d; + if(li > anchor_len){ + d = (float)BPM(si, anchor_s, li, anchor_len); + }else{ + d = (float)BPM(anchor_s, si, anchor_len, li); + } + if(d < min_dist[i]){ + min_dist[i] = d; + } + } + } - /* num_anchor = MACRO_MAX(MACRO_MIN(32, msa->numseq), (int) pow(log2((double) msa->numseq), 2.0)); */ - num_anchor = MACRO_MIN(32, msa->numseq); - RUNP(anchors = select_seqs(msa, num_anchor)); - *n = num_anchor; + MFREE(min_dist); + *n_out = K; return anchors; ERROR: + if(anchors) MFREE(anchors); + if(min_dist) MFREE(min_dist); return NULL; } -int* select_seqs(struct msa* msa, int num_anchor) +int* pick_anchor(struct msa* msa, int* n) { - struct sort_struct** seq_sort = NULL; - int* anchors = NULL; - int i,stride; - - MMALLOC(seq_sort, sizeof(struct sort_struct*) * msa->numseq); - for(i = 0; i < msa->numseq;i++){ - seq_sort[i] = NULL; - MMALLOC(seq_sort[i], sizeof(struct sort_struct)); - seq_sort[i]->id = i; - seq_sort[i]->len = msa->sequences[i]->len;// aln->sl[i]; - } - - qsort(seq_sort, msa->numseq, sizeof(struct sort_struct*),sort_by_len); - /* for(i = 0; i < msa->numseq;i++){ */ - /* fprintf(stdout,"%d\t%d id: %d \n", seq_sort[i]->id,seq_sort[i]->len,seq_sort[i]->id); */ - /* } */ - - - //fprintf(stdout,"%d\t seeds\n", num_anchor); - - MMALLOC(anchors, sizeof(int) * num_anchor); - stride = msa->numseq / num_anchor; -// fprintf(stdout,"%d\tstride\n", stride); - //c = 0; - for(i = 0; i < num_anchor;i++){ - anchors[i] = seq_sort[i*stride]->id; - /* LOG_MSG("Anchor: %d",anchors[i] ); */ - } - ASSERT(i == num_anchor,"Cound not select all anchors\tnum_anchor:%d\t numseq:%d",num_anchor,msa->numseq); - - for(i = 0; i < msa->numseq;i++){ - MFREE(seq_sort[i]); - } - MFREE(seq_sort); - return anchors; + ASSERT(msa != NULL, "No alignment."); + return pick_anchor_farthest_first(msa, DEFAULT_NUM_ANCHORS, n); ERROR: return NULL; } -int sort_by_len(const void *a, const void *b) +int* pick_anchor_n(struct msa* msa, int requested, int* n) { - struct sort_struct* const *one = a; - struct sort_struct* const *two = b; - - if((*one)->len > (*two)->len){ - return -1; - }else{ - return 1; - } + ASSERT(msa != NULL, "No alignment."); + return pick_anchor_farthest_first(msa, requested, n); +ERROR: + return NULL; } diff --git a/lib/src/pick_anchor.h b/lib/src/pick_anchor.h index 5aa4093..938c462 100644 --- a/lib/src/pick_anchor.h +++ b/lib/src/pick_anchor.h @@ -14,6 +14,7 @@ struct msa; EXTERN int* pick_anchor(struct msa* msa, int* n); +EXTERN int* pick_anchor_n(struct msa* msa, int requested, int* n); #undef PICK_ANCHOR_IMPORT diff --git a/lib/src/sequence_distance.c b/lib/src/sequence_distance.c index 1e71eb6..eb25684 100644 --- a/lib/src/sequence_distance.c +++ b/lib/src/sequence_distance.c @@ -55,17 +55,11 @@ float** d_estimation(struct msa* msa, int* samples, int num_samples,int pair) RUN(galloc(&dm,num_samples,num_samples)); for(i = 0; i < num_samples;i++){ - seq_a = msa->sequences[samples[i]]->s;// aln->s[samples[i]]; - len_a = msa->sequences[samples[i]]->len;//aln->sl[samples[i]]; + seq_a = msa->sequences[samples[i]]->s; + len_a = msa->sequences[samples[i]]->len; for(j = 0;j < num_samples;j++){ - //fprintf(stdout, "Working on %d %d\n", i,j); - - seq_b = msa->sequences[samples[j]]->s; //aln->s[ samples[j]]; - len_b = msa->sequences[samples[j]]->len;//aln->sl[selection[j]]; - /*dm[i][j] = MACRO_MIN(len_a, len_b) - MACRO_MIN( - bpm_256(seq_a, seq_b, len_a, len_b), - bpm_256(seq_b, seq_a, len_b, len_a) - );*/ + seq_b = msa->sequences[samples[j]]->s; + len_b = msa->sequences[samples[j]]->len; dist = calc_distance(seq_a, seq_b, len_a, len_b); /* give shorter sequences a preference */ int s = (len_a + len_b) / 2; diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index 02fefd0..d45c9fc 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -84,7 +84,7 @@ def __repr__(self): MODE_PRECISE = "precise" # deprecated alias for "accurate" # Valid preset modes (resolved by C library) -_PRESET_MODES = {"fast", "default", "accurate"} +_PRESET_MODES = {"fast", "default", "recall", "accurate"} # Global thread control _thread_local = threading.local() diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index d78df4a..ef92e4c 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -246,7 +246,9 @@ void ensemble_custom_file_to_file( const std::vector& run_refine = {}, const std::vector& run_realign = {}, const std::vector& run_consistency_anchors = {}, - const std::vector& run_consistency_weight = {} + const std::vector& run_consistency_weight = {}, + int consistency_merge = 0, + float consistency_merge_weight = 2.0f ) { int n_runs = static_cast(run_gpo.size()); if (n_runs < 1) { @@ -308,6 +310,8 @@ void ensemble_custom_file_to_file( struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); ens.min_support = min_support; + ens.consistency_merge = consistency_merge; + ens.consistency_merge_weight = consistency_merge_weight; result = kalign_align_full(msa_data, runs.data(), n_runs, &ens, n_threads); if (result != 0) { @@ -358,7 +362,9 @@ py::object align_mode( msa_data->quiet = 1; /* Force biotype if caller specified one */ - if (seq_type == KALIGN_MATRIX_DNA || seq_type == KALIGN_MATRIX_DNA_INTERNAL) { + if (seq_type == KALIGN_MATRIX_DNA || seq_type == KALIGN_MATRIX_DNA_INTERNAL || + seq_type == KALIGN_MATRIX_NUC_1PAM || seq_type == KALIGN_MATRIX_NUC_20PAM || + seq_type == KALIGN_MATRIX_NUC_200PAM) { msa_data->biotype = ALN_BIOTYPE_DNA; } else if (seq_type == KALIGN_MATRIX_RNA) { msa_data->biotype = ALN_BIOTYPE_DNA; /* RNA uses DNA biotype internally */ @@ -722,6 +728,8 @@ PYBIND11_MODULE(_core, m) { py::arg("run_realign") = std::vector{}, py::arg("run_consistency_anchors") = std::vector{}, py::arg("run_consistency_weight") = std::vector{}, + py::arg("consistency_merge") = 0, + py::arg("consistency_merge_weight") = 2.0f, R"pbdoc( Ensemble alignment with per-run parameters. @@ -832,6 +840,9 @@ PYBIND11_MODULE(_core, m) { m.attr("MATRIX_DNA") = KALIGN_MATRIX_DNA; m.attr("MATRIX_DNA_INTERNAL") = KALIGN_MATRIX_DNA_INTERNAL; m.attr("MATRIX_RNA") = KALIGN_MATRIX_RNA; + m.attr("MATRIX_NUC_1PAM") = KALIGN_MATRIX_NUC_1PAM; + m.attr("MATRIX_NUC_20PAM") = KALIGN_MATRIX_NUC_20PAM; + m.attr("MATRIX_NUC_200PAM") = KALIGN_MATRIX_NUC_200PAM; // Backward compat: old names point to new values m.attr("DNA") = KALIGN_TYPE_DNA; From 9208173702d3d40256a4d07bd9d539dbfa50bef8 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Thu, 19 Mar 2026 19:20:35 +0800 Subject: [PATCH 05/29] Unified nucleotide presets: replace separate RNA/DNA with single preset_nucleotide() Replaces preset_rna() and preset_dna() with a single preset_nucleotide() optimized on combined BRAliBASE (599 RNA) + MDSA (325 DNA) dataset. Nucleotide presets (combined BRAliBASE + MDSA, gen 100): fast: R=0.792 P=0.788 F1=0.790 5s (single K200, realign=1) default: R=0.773 P=0.842 F1=0.806 26s (ens3 K20/K200, realign=1) recall: R=0.800 P=0.796 F1=0.798 17s (single K200, realign=1, refine=C) accurate: R=0.760 P=0.867 F1=0.810 100s (ens5 K200/K1, realign=1, ms=3) Dispatch is now: protein vs nucleotide (no RNA/DNA distinction). Kalign auto-detects sequence type; user only picks mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/src/aln_wrap.c | 206 ++++++++++++--------------------------------- 1 file changed, 52 insertions(+), 154 deletions(-) diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index 848e30d..d3c78dd 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -881,188 +881,90 @@ static int preset_protein(const char *m, return -1; } -/* ---- RNA presets (NSGA-III optimized on BRAliBASE, gen 88) ---- +/* ---- Nucleotide presets (NSGA-III optimized on BRAliBASE + MDSA, gen 100) ---- * - * NOTE: fast ≈ recall (both single-run K200, nearly identical params). - * RNA alignments are "easy" enough that a single run captures - * most of the quality. Kept separate for future tuning. + * Unified presets for both DNA and RNA. Optimized on combined + * BRAliBASE (599 RNA cases) + MDSA (325 DNA cases). + * All presets use Kimura two-parameter matrices. */ -static int preset_rna(const char *m, - struct kalign_run_config *runs, - int *n_runs, - struct kalign_ensemble_config *ens) +static int preset_nucleotide(const char *m, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens) { - /* fast: single run, ~5s on BRAliBASE. F1=0.828 R=0.832 P=0.825 */ + /* fast: single run with realign, ~5s. F1=0.790 R=0.792 P=0.788 */ if(strcasecmp(m, "fast") == 0){ *n_runs = 1; preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 11.939f, 1.598f, 2.890f, - 0.636f, 1.363f, - 0, KALIGN_REFINE_INLINE, + 19.873f, 0.642f, 2.582f, + 0.874f, 0.199f, + 1, KALIGN_REFINE_NONE, 0, 0.0f); - preset_consistency(runs, 1, 3, 2.372f); return 0; } - /* default: 3-run ensemble, ~8s. F1=0.835 R=0.804 P=0.869 */ + /* default: 3-run ensemble with realign, ~26s. + F1=0.806 R=0.773 P=0.842 */ if(strcasecmp(m, "default") == 0){ *n_runs = 3; - preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 19.633f, 0.582f, 2.974f, - 1.929f, 1.569f, - 0, KALIGN_REFINE_NONE, 42, 0.0f); + preset_run(&runs[0], KALIGN_MATRIX_NUC_20PAM, + 19.275f, 0.628f, 2.854f, + 0.445f, 0.448f, + 1, KALIGN_REFINE_NONE, 42, 0.0f); preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, - 13.155f, 1.631f, 2.717f, - 0.733f, 1.569f, - 0, KALIGN_REFINE_NONE, 43, 0.0f); - preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, - 18.052f, 0.391f, 2.316f, - 0.598f, 1.569f, - 0, KALIGN_REFINE_NONE, 44, 0.0f); - preset_consistency(runs, 3, 1, 2.636f); + 16.581f, 0.274f, 2.491f, + 0.706f, 0.448f, + 1, KALIGN_REFINE_NONE, 43, 0.0f); + preset_run(&runs[2], KALIGN_MATRIX_NUC_200PAM, + 16.499f, 3.754f, 2.896f, + 0.491f, 0.448f, + 1, KALIGN_REFINE_CONFIDENT, 44, 0.0f); ens->min_support = 2; return 0; } - /* recall: single run, ~6s. F1=0.829 R=0.833 P=0.826 */ + /* recall: single run with realign, ~17s. + F1=0.798 R=0.800 P=0.796 */ if(strcasecmp(m, "recall") == 0){ *n_runs = 1; preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 11.939f, 1.591f, 2.890f, - 0.065f, 1.363f, - 0, KALIGN_REFINE_CONFIDENT, + 19.882f, 0.646f, 2.854f, + 0.874f, 0.449f, + 1, KALIGN_REFINE_CONFIDENT, 0, 0.0f); - preset_consistency(runs, 1, 3, 2.572f); + preset_consistency(runs, 1, 1, 1.616f); return 0; } - /* accurate: 3-run ensemble with realign, ~26s. - F1=0.836 R=0.811 P=0.863 */ + /* accurate: 5-run ensemble with realign, ~100s. + F1=0.810 R=0.760 P=0.867 */ if(strcasecmp(m, "accurate") == 0){ - *n_runs = 3; - preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 10.799f, 1.389f, 2.718f, - 0.094f, 0.364f, - 2, KALIGN_REFINE_CONFIDENT, 42, 0.0f); - preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, - 15.595f, 1.932f, 2.486f, - 0.190f, 0.364f, - 2, KALIGN_REFINE_ALL, 43, 0.0f); - preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, - 14.749f, 1.041f, 2.450f, - 2.712f, 0.364f, - 2, KALIGN_REFINE_CONFIDENT, 44, 0.0f); - preset_consistency(runs, 3, 3, 2.634f); - ens->min_support = 2; - return 0; - } - - return -1; -} - -/* ---- DNA presets (NSGA-III optimized on MDSA, gen 100) ---- - * - * NOTE: default = accurate (optimizer converged on same solution). - * DNA alignments are harder than RNA; even "fast" needs ens3+realign. - * Kept separate for future tuning. - */ - -static int preset_dna(const char *m, - struct kalign_run_config *runs, - int *n_runs, - struct kalign_ensemble_config *ens) -{ - /* fast: 3-run ensemble with realign, ~18s on MDSA. - F1=0.764 R=0.741 P=0.788 */ - if(strcasecmp(m, "fast") == 0){ - *n_runs = 3; - preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 19.660f, 0.600f, 2.690f, - 1.050f, 2.710f, - 1, KALIGN_REFINE_NONE, 42, 0.0f); - preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, - 19.470f, 1.600f, 2.490f, - 0.140f, 2.710f, - 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); - preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, - 17.200f, 3.810f, 2.770f, - 0.070f, 2.710f, - 1, KALIGN_REFINE_NONE, 44, 0.0f); - ens->min_support = 2; - return 0; - } - - /* default: 3-run ensemble with realign, ~35s. - F1=0.775 R=0.737 P=0.816 */ - if(strcasecmp(m, "default") == 0){ - *n_runs = 3; - preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 19.350f, 0.310f, 2.860f, - 1.450f, 1.830f, - 1, KALIGN_REFINE_INLINE, 42, 0.0f); - preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, - 19.020f, 1.980f, 2.510f, - 0.490f, 1.830f, - 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); - preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, - 17.850f, 3.000f, 2.450f, - 1.380f, 1.830f, - 1, KALIGN_REFINE_NONE, 44, 0.0f); - preset_consistency(runs, 3, 1, 0.780f); - ens->min_support = 2; - return 0; - } - - /* recall: 5-run ensemble with realign, ~65s. - F1=0.765 R=0.760 P=0.770 */ - if(strcasecmp(m, "recall") == 0){ *n_runs = 5; preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 19.370f, 0.660f, 2.650f, - 2.470f, 1.820f, - 1, KALIGN_REFINE_INLINE, 42, 0.0f); - preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, - 19.110f, 1.580f, 2.490f, - 1.060f, 1.820f, + 19.475f, 0.728f, 2.856f, + 0.492f, 0.590f, + 1, KALIGN_REFINE_CONFIDENT, 42, 0.0f); + preset_run(&runs[1], KALIGN_MATRIX_NUC_1PAM, + 19.377f, 1.379f, 2.486f, + 1.211f, 0.590f, 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); preset_run(&runs[2], KALIGN_MATRIX_NUC_200PAM, - 16.730f, 1.190f, 2.480f, - 0.360f, 1.820f, - 1, KALIGN_REFINE_CONFIDENT, 44, 0.0f); - preset_run(&runs[3], KALIGN_MATRIX_NUC_200PAM, - 16.730f, 4.090f, 2.200f, - 0.890f, 1.820f, - 1, KALIGN_REFINE_NONE, 45, 0.0f); - preset_run(&runs[4], KALIGN_MATRIX_NUC_200PAM, - 9.340f, 1.380f, 1.100f, - 1.400f, 1.820f, - 1, KALIGN_REFINE_CONFIDENT, 46, 0.0f); - preset_consistency(runs, 5, 1, 0.630f); - ens->min_support = 2; - return 0; - } - - /* accurate: same as default (optimizer converged on same solution). - F1=0.775 R=0.737 P=0.816 */ - if(strcasecmp(m, "accurate") == 0){ - *n_runs = 3; - preset_run(&runs[0], KALIGN_MATRIX_NUC_200PAM, - 19.350f, 0.310f, 2.860f, - 1.450f, 1.830f, - 1, KALIGN_REFINE_INLINE, 42, 0.0f); - preset_run(&runs[1], KALIGN_MATRIX_NUC_200PAM, - 19.020f, 1.980f, 2.510f, - 0.490f, 1.830f, - 1, KALIGN_REFINE_CONFIDENT, 43, 0.0f); - preset_run(&runs[2], KALIGN_MATRIX_NUC_20PAM, - 17.850f, 3.000f, 2.450f, - 1.380f, 1.830f, + 15.869f, 0.353f, 2.430f, + 0.721f, 0.590f, 1, KALIGN_REFINE_NONE, 44, 0.0f); - preset_consistency(runs, 3, 1, 0.780f); - ens->min_support = 2; + preset_run(&runs[3], KALIGN_MATRIX_NUC_200PAM, + 15.989f, 2.331f, 2.764f, + 0.188f, 0.590f, + 1, KALIGN_REFINE_CONFIDENT, 45, 0.0f); + preset_run(&runs[4], KALIGN_MATRIX_NUC_1PAM, + 5.346f, 3.483f, 0.610f, + 1.406f, 0.590f, + 1, KALIGN_REFINE_NONE, 46, 0.0f); + preset_consistency(runs, 5, 2, 0.800f); + ens->min_support = 3; return 0; } @@ -1080,11 +982,7 @@ int kalign_get_mode_preset(const char *mode, *ens = kalign_ensemble_config_defaults(); if(biotype == ALN_BIOTYPE_DNA){ - /* TODO: distinguish DNA vs RNA for better dispatch. - Currently defaults to RNA presets. DNA presets are - available via preset_dna() but need RNA detection - (e.g. presence of U) to select automatically. */ - return preset_rna(m, runs, n_runs, ens); + return preset_nucleotide(m, runs, n_runs, ens); } /* Default: protein presets */ From ccaa5a9097819d1f87049ee40a821d64623a201f Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 21 Mar 2026 21:34:05 +0800 Subject: [PATCH 06/29] Chase-Lev work-stealing thread pool with tests and benchmarks Self-contained threadpool library in lib/src/threadpool/ with: - Lock-free Chase-Lev deques (per-worker LIFO) + global ext queue - Three parallelism patterns: parallel_for, fork-join groups, recursive tasks - Event-count sleeping, per-worker group recycling, work-stealing - 17 unit tests, 10 stress tests (TSan-clean), OpenMP comparison benchmarks - Standalone CMake build (also builds as part of kalign) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/src/threadpool/CMakeLists.txt | 41 ++ lib/src/threadpool/bench_threadpool.c | 538 +++++++++++++++++++ lib/src/threadpool/stress_threadpool.c | 625 ++++++++++++++++++++++ lib/src/threadpool/test_threadpool.c | 579 ++++++++++++++++++++ lib/src/threadpool/threadpool.c | 709 +++++++++++++++++++++++++ lib/src/threadpool/threadpool.h | 139 +++++ 6 files changed, 2631 insertions(+) create mode 100644 lib/src/threadpool/CMakeLists.txt create mode 100644 lib/src/threadpool/bench_threadpool.c create mode 100644 lib/src/threadpool/stress_threadpool.c create mode 100644 lib/src/threadpool/test_threadpool.c create mode 100644 lib/src/threadpool/threadpool.c create mode 100644 lib/src/threadpool/threadpool.h diff --git a/lib/src/threadpool/CMakeLists.txt b/lib/src/threadpool/CMakeLists.txt new file mode 100644 index 0000000..2039972 --- /dev/null +++ b/lib/src/threadpool/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.18) + +# Support standalone build +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(threadpool LANGUAGES C) + set(CMAKE_C_STANDARD 11) + set(CMAKE_C_STANDARD_REQUIRED ON) + enable_testing() + + # Optional: find OpenMP for benchmark comparison + find_package(OpenMP) +endif() + +find_package(Threads REQUIRED) + +# ── Thread pool library ─────────────────────────────────────────── +add_library(threadpool STATIC threadpool.c) +target_include_directories(threadpool PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(threadpool PUBLIC Threads::Threads) +set_target_properties(threadpool PROPERTIES C_STANDARD 11) + +# ── Tests ───────────────────────────────────────────────────────── +add_executable(test_threadpool test_threadpool.c) +target_link_libraries(test_threadpool PRIVATE threadpool) +set_target_properties(test_threadpool PROPERTIES C_STANDARD 11) +add_test(NAME threadpool_tests COMMAND test_threadpool) + +add_executable(stress_threadpool stress_threadpool.c) +target_link_libraries(stress_threadpool PRIVATE threadpool) +set_target_properties(stress_threadpool PROPERTIES C_STANDARD 11) +add_test(NAME threadpool_stress COMMAND stress_threadpool) + +# ── Benchmarks ──────────────────────────────────────────────────── +add_executable(bench_threadpool bench_threadpool.c) +target_link_libraries(bench_threadpool PRIVATE threadpool) +set_target_properties(bench_threadpool PROPERTIES C_STANDARD 11) + +if(OpenMP_C_FOUND) + target_link_libraries(bench_threadpool PRIVATE OpenMP::OpenMP_C) + target_compile_definitions(bench_threadpool PRIVATE HAVE_OPENMP) +endif() diff --git a/lib/src/threadpool/bench_threadpool.c b/lib/src/threadpool/bench_threadpool.c new file mode 100644 index 0000000..e9292ea --- /dev/null +++ b/lib/src/threadpool/bench_threadpool.c @@ -0,0 +1,538 @@ +/* + * bench_threadpool.c — Benchmarks for the three kalign parallelism patterns. + * + * 1. Distance matrix (parallel for) + * 2. Recursive tree (task spawning) + * 3. Fork-join 2 tasks (Hirschberg-like) + * + * Each benchmark compares serial, threadpool (1..N threads), + * and optionally OpenMP. + */ + +#include "threadpool.h" +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_OPENMP +#include +#endif + +/* ── Timing ───────────────────────────────────────────────────── */ + +static double now(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int cmp_double(const void *a, const void *b) +{ + double da = *(const double *)a; + double db = *(const double *)b; + return (da > db) - (da < db); +} + +#define NREPS 7 + +static double median(double *arr, int n) +{ + qsort(arr, (size_t)n, sizeof(double), cmp_double); + return arr[n / 2]; +} + +static void fmt_time(double secs, char *buf, int len) +{ + if (secs < 1e-3) + snprintf(buf, (size_t)len, "%7.1f us", secs * 1e6); + else if (secs < 1.0) + snprintf(buf, (size_t)len, "%7.2f ms", secs * 1e3); + else + snprintf(buf, (size_t)len, "%7.3f s ", secs); +} + +/* ── Workload helpers ─────────────────────────────────────────── */ + +static uint8_t **create_sequences(int n, int len) +{ + uint8_t **seqs = (uint8_t **)malloc((size_t)n * sizeof(uint8_t *)); + for (int i = 0; i < n; i++) { + seqs[i] = (uint8_t *)malloc((size_t)len); + unsigned x = (unsigned)i * 2654435761u; + for (int j = 0; j < len; j++) { + x = x * 1103515245u + 12345u; + seqs[i][j] = (uint8_t)((x >> 16) % 20); + } + } + return seqs; +} + +static void free_sequences(uint8_t **seqs, int n) +{ + for (int i = 0; i < n; i++) free(seqs[i]); + free(seqs); +} + +/* ══════════════════════════════════════════════════════════════════ + * 1. DISTANCE MATRIX (parallel for) + * ══════════════════════════════════════════════════════════════════ */ + +struct dm_ctx { + float **dm; + uint8_t **seqs; + int seq_len; + int num_anchors; +}; + +static void dm_chunk(int start, int end, void *arg) +{ + struct dm_ctx *ctx = (struct dm_ctx *)arg; + for (int i = start; i < end; i++) { + for (int j = 0; j < ctx->num_anchors; j++) { + int diff = 0; + for (int k = 0; k < ctx->seq_len; k++) + diff += (ctx->seqs[i][k] != ctx->seqs[j][k]); + ctx->dm[i][j] = (float)diff / (float)ctx->seq_len; + } + } +} + +static double bench_dm_serial(uint8_t **seqs, float **dm, + int N, int M, int L) +{ + struct dm_ctx ctx = { dm, seqs, L, M }; + double t0 = now(); + dm_chunk(0, N, &ctx); + return now() - t0; +} + +static double bench_dm_tp(uint8_t **seqs, float **dm, + int N, int M, int L, int nthreads) +{ + struct dm_ctx ctx = { dm, seqs, L, M }; + threadpool_t *pool = tp_create(nthreads); + double t0 = now(); + tp_parallel_for(pool, 0, N, dm_chunk, &ctx); + double elapsed = now() - t0; + tp_destroy(pool); + return elapsed; +} + +#ifdef HAVE_OPENMP +static double bench_dm_omp(uint8_t **seqs, float **dm, + int N, int M, int L) +{ + double t0 = now(); + int i, j, k; + #pragma omp parallel for shared(dm, seqs) private(i, j, k) schedule(static) + for (i = 0; i < N; i++) { + for (j = 0; j < M; j++) { + int diff = 0; + for (k = 0; k < L; k++) + diff += (seqs[i][k] != seqs[j][k]); + dm[i][j] = (float)diff / (float)L; + } + } + return now() - t0; +} +#endif + +/* ══════════════════════════════════════════════════════════════════ + * 2. RECURSIVE TREE (task spawning) + * ══════════════════════════════════════════════════════════════════ */ + +struct tree_node { + struct tree_node *left; + struct tree_node *right; + int leaf_id; + int depth; + float result; +}; + +static struct tree_node *build_tree(int depth, int *next_id) +{ + struct tree_node *n = (struct tree_node *)calloc(1, sizeof(*n)); + n->depth = depth; + if (depth == 0) { + n->leaf_id = (*next_id)++; + return n; + } + n->leaf_id = -1; + n->left = build_tree(depth - 1, next_id); + n->right = build_tree(depth - 1, next_id); + return n; +} + +static void free_tree(struct tree_node *n) +{ + if (!n) return; + free_tree(n->left); + free_tree(n->right); + free(n); +} + +#define LEAF_ITERS 500 + +static float leaf_work(int id) +{ + float sum = 0.0f; + unsigned x = (unsigned)id * 2654435761u; + for (int i = 0; i < LEAF_ITERS; i++) { + x = x * 1103515245u + 12345u; + sum += (float)(x >> 16) / 65536.0f; + } + return sum; +} + +/* Serial */ +static void tree_serial(struct tree_node *n) +{ + if (n->leaf_id >= 0) { n->result = leaf_work(n->leaf_id); return; } + tree_serial(n->left); + tree_serial(n->right); + n->result = n->left->result + n->right->result; +} + +/* Threadpool */ +struct tree_tp_arg { + threadpool_t *pool; + struct tree_node *node; +}; + +/* Serial cutoff: avoid task creation overhead for tiny subtrees. + * Not needed for correctness (V2 Chase-Lev bounds stack to O(depth)), + * but worthwhile for performance — each task group costs a malloc. */ +#define TREE_TASK_CUTOFF 4 + +static void tree_tp_task(void *arg) +{ + struct tree_tp_arg *ta = (struct tree_tp_arg *)arg; + struct tree_node *n = ta->node; + + if (n->leaf_id >= 0) { n->result = leaf_work(n->leaf_id); return; } + + if (n->depth <= TREE_TASK_CUTOFF) { + tree_serial(n); + return; + } + + struct tree_tp_arg left = { ta->pool, n->left }; + struct tree_tp_arg right = { ta->pool, n->right }; + + tp_group_t *g = tp_group_create(ta->pool); + tp_group_submit(g, tree_tp_task, &left); + tp_group_submit(g, tree_tp_task, &right); + tp_group_wait(g); + tp_group_destroy(g); + + n->result = n->left->result + n->right->result; +} + +#ifdef HAVE_OPENMP +static void tree_omp_task(struct tree_node *n) +{ + if (n->leaf_id >= 0) { n->result = leaf_work(n->leaf_id); return; } + + #pragma omp task shared(n) + tree_omp_task(n->left); + #pragma omp task shared(n) + tree_omp_task(n->right); + #pragma omp taskwait + + n->result = n->left->result + n->right->result; +} +#endif + +/* ══════════════════════════════════════════════════════════════════ + * 3. FORK-JOIN (Hirschberg-style: 2 independent tasks) + * ══════════════════════════════════════════════════════════════════ */ + +struct fj_sum_arg { + const float *arr; + int start; + int end; + double result; +}; + +static void fj_sum_task(void *arg) +{ + struct fj_sum_arg *a = (struct fj_sum_arg *)arg; + double s = 0.0; + for (int i = a->start; i < a->end; i++) + s += (double)a->arr[i]; + a->result = s; +} + +static double bench_fj_serial(const float *arr, int N) +{ + double s = 0.0; + double t0 = now(); + for (int i = 0; i < N; i++) s += (double)arr[i]; + double elapsed = now() - t0; + (void)s; + return elapsed; +} + +static double bench_fj_tp(const float *arr, int N, int nthreads) +{ + threadpool_t *pool = tp_create(nthreads); + int mid = N / 2; + struct fj_sum_arg left = { arr, 0, mid, 0.0 }; + struct fj_sum_arg right = { arr, mid, N, 0.0 }; + + double t0 = now(); + tp_group_t *g = tp_group_create(pool); + tp_group_submit(g, fj_sum_task, &left); + tp_group_submit(g, fj_sum_task, &right); + tp_group_wait(g); + tp_group_destroy(g); + double elapsed = now() - t0; + + (void)(left.result + right.result); + tp_destroy(pool); + return elapsed; +} + +/* ══════════════════════════════════════════════════════════════════ + * 4. OVERHEAD MEASUREMENT + * ══════════════════════════════════════════════════════════════════ */ + +static void noop_task(void *arg) { (void)arg; } + +static double bench_overhead(int nthreads) +{ + threadpool_t *pool = tp_create(nthreads); + int N = 10000; + + double t0 = now(); + for (int i = 0; i < N; i++) { + tp_group_t *g = tp_group_create(pool); + tp_group_submit(g, noop_task, NULL); + tp_group_wait(g); + tp_group_destroy(g); + } + double elapsed = now() - t0; + + tp_destroy(pool); + return elapsed / N; +} + +/* ══════════════════════════════════════════════════════════════════ + * MAIN + * ══════════════════════════════════════════════════════════════════ */ + +int main(void) +{ + int ncpus = (int)sysconf(_SC_NPROCESSORS_ONLN); + if (ncpus <= 0) ncpus = 4; + + /* Thread counts to test */ + int tc[] = { 1, 2, 4, 8, 16 }; + int ntc = 0; + for (int i = 0; i < 5; i++) { + if (tc[i] <= ncpus * 2) ntc++; + } + + printf("threadpool benchmark\n"); + printf("====================\n"); + printf("CPUs: %d\n\n", ncpus); + + /* ── Overhead ─────────────────────────────────────────────── */ + { + char buf[32]; + double per_task = bench_overhead(ncpus); + fmt_time(per_task, buf, sizeof(buf)); + printf("Per-task overhead (submit+wait+destroy): %s\n\n", buf); + } + + /* ── Distance Matrix ──────────────────────────────────────── */ + { + struct { int N, M, L; } sizes[] = { + { 500, 50, 200 }, + { 2000, 100, 200 }, + { 5000, 100, 200 }, + }; + int nsizes = 3; + + printf("--- Distance Matrix (parallel for) ---\n"); + printf(" %-14s %10s", "Size", "serial"); + for (int t = 0; t < ntc; t++) + printf(" tp(%2d) ", tc[t]); +#ifdef HAVE_OPENMP + printf(" omp "); +#endif + printf("\n"); + + for (int s = 0; s < nsizes; s++) { + int N = sizes[s].N, M = sizes[s].M, L = sizes[s].L; + + uint8_t **seqs = create_sequences(N, L); + float **dm = (float **)malloc((size_t)N * sizeof(float *)); + for (int i = 0; i < N; i++) + dm[i] = (float *)calloc((size_t)M, sizeof(float)); + + char label[32]; + snprintf(label, sizeof(label), "%dx%d", N, M); + printf(" %-14s", label); + + /* Serial */ + double times[NREPS]; + for (int r = 0; r < NREPS; r++) + times[r] = bench_dm_serial(seqs, dm, N, M, L); + double serial_t = median(times, NREPS); + char buf[32]; + fmt_time(serial_t, buf, sizeof(buf)); + printf(" %s", buf); + + /* Threadpool */ + for (int t = 0; t < ntc; t++) { + for (int r = 0; r < NREPS; r++) + times[r] = bench_dm_tp(seqs, dm, N, M, L, tc[t]); + double tp_t = median(times, NREPS); + fmt_time(tp_t, buf, sizeof(buf)); + printf(" %s", buf); + } + +#ifdef HAVE_OPENMP + for (int r = 0; r < NREPS; r++) + times[r] = bench_dm_omp(seqs, dm, N, M, L); + double omp_t = median(times, NREPS); + fmt_time(omp_t, buf, sizeof(buf)); + printf(" %s", buf); +#endif + printf("\n"); + + for (int i = 0; i < N; i++) free(dm[i]); + free(dm); + free_sequences(seqs, N); + } + printf("\n"); + } + + /* ── Recursive Tree ───────────────────────────────────────── */ + { + int depths[] = { 10, 14, 17 }; + int ndepths = 3; + + printf("--- Recursive Tree (task spawning) ---\n"); + printf(" %-14s %10s", "Depth/Leaves", "serial"); + for (int t = 0; t < ntc; t++) + printf(" tp(%2d) ", tc[t]); +#ifdef HAVE_OPENMP + printf(" omp "); +#endif + printf("\n"); + + for (int d = 0; d < ndepths; d++) { + int depth = depths[d]; + int nleaves = 1 << depth; + + char label[32]; + snprintf(label, sizeof(label), "d=%d (%d)", depth, nleaves); + printf(" %-14s", label); + + double times[NREPS]; + char buf[32]; + + /* Serial */ + for (int r = 0; r < NREPS; r++) { + int id = 0; + struct tree_node *root = build_tree(depth, &id); + double t0 = now(); + tree_serial(root); + times[r] = now() - t0; + free_tree(root); + } + fmt_time(median(times, NREPS), buf, sizeof(buf)); + printf(" %s", buf); + + /* Threadpool */ + for (int t = 0; t < ntc; t++) { + for (int r = 0; r < NREPS; r++) { + int id = 0; + struct tree_node *root = build_tree(depth, &id); + threadpool_t *pool = tp_create(tc[t]); + struct tree_tp_arg targ = { pool, root }; + double t0 = now(); + tree_tp_task(&targ); + times[r] = now() - t0; + tp_destroy(pool); + free_tree(root); + } + fmt_time(median(times, NREPS), buf, sizeof(buf)); + printf(" %s", buf); + } + +#ifdef HAVE_OPENMP + for (int r = 0; r < NREPS; r++) { + int id = 0; + struct tree_node *root = build_tree(depth, &id); + double t0 = now(); + #pragma omp parallel + #pragma omp single nowait + tree_omp_task(root); + times[r] = now() - t0; + free_tree(root); + } + fmt_time(median(times, NREPS), buf, sizeof(buf)); + printf(" %s", buf); +#endif + printf("\n"); + } + printf("\n"); + } + + /* ── Fork-Join ────────────────────────────────────────────── */ + { + int fsizes[] = { 10000, 100000, 1000000, 10000000 }; + int nfsizes = 4; + + printf("--- Fork-Join (2 tasks, array sum) ---\n"); + printf(" %-14s %10s", "N", "serial"); + for (int t = 0; t < ntc; t++) + printf(" tp(%2d) ", tc[t]); + printf("\n"); + + for (int s = 0; s < nfsizes; s++) { + int N = fsizes[s]; + float *arr = (float *)malloc((size_t)N * sizeof(float)); + unsigned x = 12345; + for (int i = 0; i < N; i++) { + x = x * 1103515245u + 12345u; + arr[i] = (float)(x >> 16) / 65536.0f; + } + + char label[32]; + if (N >= 1000000) + snprintf(label, sizeof(label), "%dM", N / 1000000); + else + snprintf(label, sizeof(label), "%dK", N / 1000); + printf(" %-14s", label); + + double times[NREPS]; + char buf[32]; + + for (int r = 0; r < NREPS; r++) + times[r] = bench_fj_serial(arr, N); + fmt_time(median(times, NREPS), buf, sizeof(buf)); + printf(" %s", buf); + + for (int t = 0; t < ntc; t++) { + for (int r = 0; r < NREPS; r++) + times[r] = bench_fj_tp(arr, N, tc[t]); + fmt_time(median(times, NREPS), buf, sizeof(buf)); + printf(" %s", buf); + } + printf("\n"); + + free(arr); + } + printf("\n"); + } + + return 0; +} diff --git a/lib/src/threadpool/stress_threadpool.c b/lib/src/threadpool/stress_threadpool.c new file mode 100644 index 0000000..2966d5c --- /dev/null +++ b/lib/src/threadpool/stress_threadpool.c @@ -0,0 +1,625 @@ +/* + * stress_threadpool.c — Stress tests for production readiness. + * + * Covers scenarios the unit tests don't: + * 1. High contention (many submitters, few workers) + * 2. Rapid pool create/destroy cycles + * 3. Deque overflow → ext queue fallback + * 4. Oversubscription (more workers than cores) + * 5. Sustained mixed-pattern load + * 6. Shutdown during active work (tp_request_shutdown) + * 7. Correctness under sustained parallel-for pressure + */ + +#include "threadpool.h" +#include +#include +#include +#include +#include +#include + +/* ── TSan detection ────────────────────────────────────────────── + * TSan can't trace happens-before through the threadpool's fence- + * based synchronization to user data. We add explicit annotations + * for tree-structured tests where the main thread writes data that + * workers later read through a chain of task submissions. */ +#if defined(__SANITIZE_THREAD__) +#define STRESS_TSAN 1 +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) +#define STRESS_TSAN 1 +#endif +#endif + +#ifdef STRESS_TSAN +void __tsan_acquire(void *addr); +void __tsan_release(void *addr); +#define TSAN_RELEASE(addr) __tsan_release(addr) +#define TSAN_ACQUIRE(addr) __tsan_acquire(addr) +#else +#define TSAN_RELEASE(addr) ((void)0) +#define TSAN_ACQUIRE(addr) ((void)0) +#endif + +/* ── Test harness ─────────────────────────────────────────────── */ + +static int g_pass, g_fail, g_total; + +#define CHECK(cond) do { \ + if (!(cond)) { \ + fprintf(stderr, " FAIL: %s (line %d)\n", \ + #cond, __LINE__); \ + return -1; \ + } \ +} while (0) + +#define RUN(name) do { \ + printf(" %-50s ", #name); \ + fflush(stdout); \ + g_total++; \ + if (test_##name() == 0) { printf("PASS\n"); g_pass++; } \ + else { printf("FAIL\n"); g_fail++; } \ +} while (0) + +static double now_sec(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +/* ── 1. High contention: many tasks from main thread ─────────── */ +/* All tasks go through ext queue since the main thread is not a + * worker. Stresses ext queue locking under contention. */ + +static void atomic_inc(void *arg) +{ + atomic_int *c = (atomic_int *)arg; + atomic_fetch_add(c, 1); +} + +static int test_high_contention_ext_queue(void) +{ + int N = 100000; + threadpool_t *pool = tp_create(8); + CHECK(pool != NULL); + + atomic_int counter; + atomic_init(&counter, 0); + + tp_group_t *g = tp_group_create(pool); + CHECK(g != NULL); + for (int i = 0; i < N; i++) { + int rc = tp_group_submit(g, atomic_inc, &counter); + CHECK(rc == 0); + } + tp_group_wait(g); + tp_group_destroy(g); + + CHECK(atomic_load(&counter) == N); + tp_destroy(pool); + return 0; +} + +/* ── 2. Rapid pool create/destroy cycles ─────────────────────── */ +/* Catches resource leaks (threads, mutexes, memory). */ + +static int test_rapid_create_destroy(void) +{ + for (int i = 0; i < 200; i++) { + threadpool_t *pool = tp_create(4); + CHECK(pool != NULL); + + /* Do a tiny bit of work each time to exercise the full path. */ + atomic_int counter; + atomic_init(&counter, 0); + tp_group_t *g = tp_group_create(pool); + CHECK(g != NULL); + tp_group_submit(g, atomic_inc, &counter); + tp_group_wait(g); + tp_group_destroy(g); + CHECK(atomic_load(&counter) == 1); + + tp_destroy(pool); + } + return 0; +} + +/* ── 3. Deque overflow → ext queue fallback ──────────────────── */ +/* A worker task submits > DEQUE_DEFAULT_CAP (4096) children. + * Once the deque fills, remaining tasks must go to the ext queue. + * All tasks must still execute correctly. */ + +struct burst_ctx { + threadpool_t *pool; + atomic_int *counter; + int burst_size; +}; + +static void burst_parent(void *arg) +{ + struct burst_ctx *ctx = (struct burst_ctx *)arg; + tp_group_t *g = tp_group_create(ctx->pool); + if (!g) return; + + for (int i = 0; i < ctx->burst_size; i++) { + tp_group_submit(g, atomic_inc, ctx->counter); + } + tp_group_wait(g); + tp_group_destroy(g); +} + +static int test_deque_overflow_fallback(void) +{ + threadpool_t *pool = tp_create(2); + CHECK(pool != NULL); + + atomic_int counter; + atomic_init(&counter, 0); + + /* Submit 8000 tasks from a worker context — exceeds deque cap of 4096. */ + struct burst_ctx ctx = { pool, &counter, 8000 }; + + tp_group_t *g = tp_group_create(pool); + CHECK(g != NULL); + tp_group_submit(g, burst_parent, &ctx); + tp_group_wait(g); + tp_group_destroy(g); + + CHECK(atomic_load(&counter) == 8000); + tp_destroy(pool); + return 0; +} + +/* ── 4. Oversubscription ─────────────────────────────────────── */ +/* More workers than CPU cores. Must not deadlock or starve. */ + +static void fill_val(int start, int end, void *arg) +{ + int *a = (int *)arg; + for (int i = start; i < end; i++) + a[i] = i + 1; +} + +static int test_oversubscription(void) +{ + int ncpus = (int)sysconf(_SC_NPROCESSORS_ONLN); + if (ncpus <= 0) ncpus = 4; + int nthreads = ncpus * 4; /* 4x oversubscription */ + + threadpool_t *pool = tp_create(nthreads); + CHECK(pool != NULL); + CHECK(tp_get_nthreads(pool) == nthreads); + + /* Parallel for with many chunks. */ + int N = 10000; + int *arr = (int *)calloc((size_t)N, sizeof(int)); + CHECK(arr != NULL); + + tp_parallel_for(pool, 0, N, fill_val, arr); + + for (int i = 0; i < N; i++) + CHECK(arr[i] == i + 1); + + free(arr); + tp_destroy(pool); + return 0; +} + +/* ── 5. Sustained mixed-pattern load ─────────────────────────── */ +/* Run parallel-for + recursive tasks + fork-join concurrently + * on the same pool over many iterations. */ + +struct tree_node_s { + struct tree_node_s *left; + struct tree_node_s *right; + int leaf_id; + atomic_int result; +}; + +static struct tree_node_s *build_small_tree(int depth, int *next_id) +{ + struct tree_node_s *n = (struct tree_node_s *)calloc(1, sizeof(*n)); + if (!n) return NULL; + atomic_init(&n->result, 0); + if (depth == 0) { + n->leaf_id = (*next_id)++; + return n; + } + n->leaf_id = -1; + n->left = build_small_tree(depth - 1, next_id); + n->right = build_small_tree(depth - 1, next_id); + return n; +} + +static void free_small_tree(struct tree_node_s *n) +{ + if (!n) return; + free_small_tree(n->left); + free_small_tree(n->right); + free(n); +} + +struct stree_arg { + threadpool_t *pool; + struct tree_node_s *node; + struct tree_node_s *root; /* sync point for TSan */ +}; + +static void stree_task(void *arg) +{ + struct stree_arg *ta = (struct stree_arg *)arg; + struct tree_node_s *n = ta->node; + TSAN_ACQUIRE(ta->root); /* pairs with TSAN_RELEASE after tree build */ + + if (n->leaf_id >= 0) { + atomic_store(&n->result, n->leaf_id + 1); + return; + } + + struct stree_arg left = { ta->pool, n->left, ta->root }; + struct stree_arg right = { ta->pool, n->right, ta->root }; + + tp_group_t *g = tp_group_create(ta->pool); + if (!g) return; + tp_group_submit(g, stree_task, &left); + tp_group_submit(g, stree_task, &right); + tp_group_wait(g); + tp_group_destroy(g); + + atomic_store(&n->result, + atomic_load(&n->left->result) + + atomic_load(&n->right->result)); +} + +static int serial_tree_sum(struct tree_node_s *n) +{ + if (n->leaf_id >= 0) return n->leaf_id + 1; + return serial_tree_sum(n->left) + serial_tree_sum(n->right); +} + +static void pfor_fill(int start, int end, void *arg) +{ + int *a = (int *)arg; + for (int i = start; i < end; i++) + a[i] = i * i; +} + +static int test_sustained_mixed_patterns(void) +{ + threadpool_t *pool = tp_create(8); + CHECK(pool != NULL); + + int N = 5000; + int *arr = (int *)calloc((size_t)N, sizeof(int)); + CHECK(arr != NULL); + + for (int iter = 0; iter < 50; iter++) { + /* Parallel for */ + memset(arr, 0, (size_t)N * sizeof(int)); + tp_parallel_for(pool, 0, N, pfor_fill, arr); + for (int i = 0; i < N; i++) + CHECK(arr[i] == i * i); + + /* Recursive tree */ + int id = 0; + struct tree_node_s *root = build_small_tree(8, &id); /* 256 leaves */ + CHECK(root != NULL); + int expected = serial_tree_sum(root); + + TSAN_RELEASE(root); /* tree is fully built; pairs with ACQUIRE in stree_task */ + struct stree_arg targ = { pool, root, root }; + tp_group_t *tg = tp_group_create(pool); + CHECK(tg != NULL); + tp_group_submit(tg, stree_task, &targ); + tp_group_wait(tg); + tp_group_destroy(tg); + CHECK(atomic_load(&root->result) == expected); + free_small_tree(root); + + /* Fork-join */ + atomic_int counter; + atomic_init(&counter, 0); + tp_group_t *g = tp_group_create(pool); + CHECK(g != NULL); + for (int i = 0; i < 100; i++) + tp_group_submit(g, atomic_inc, &counter); + tp_group_wait(g); + tp_group_destroy(g); + CHECK(atomic_load(&counter) == 100); + } + + free(arr); + tp_destroy(pool); + return 0; +} + +/* ── 6. tp_request_shutdown during active work ───────────────── */ +/* Verify that tp_group_wait returns after shutdown is requested, + * and that tp_destroy completes without hanging. */ + +static void slow_task(void *arg) +{ + atomic_int *counter = (atomic_int *)arg; + /* Simulate some work. */ + volatile int sink = 0; + for (int i = 0; i < 10000; i++) + sink += i; + (void)sink; + atomic_fetch_add(counter, 1); +} + +static int test_shutdown_during_work(void) +{ + threadpool_t *pool = tp_create(4); + CHECK(pool != NULL); + + atomic_int counter; + atomic_init(&counter, 0); + + /* Submit a batch of tasks. */ + tp_group_t *g = tp_group_create(pool); + CHECK(g != NULL); + for (int i = 0; i < 1000; i++) + tp_group_submit(g, slow_task, &counter); + + /* Request shutdown immediately — some tasks may not finish. */ + tp_request_shutdown(pool); + + /* tp_group_wait should return (possibly early). */ + tp_group_wait(g); + tp_group_destroy(g); + + /* tp_destroy should not hang. */ + tp_destroy(pool); + + /* At least some tasks should have run. */ + int completed = atomic_load(&counter); + CHECK(completed > 0); + + return 0; +} + +/* ── 7. Parallel-for correctness under sustained pressure ────── */ +/* Many iterations of parallel-for on the same pool, verifying + * every element every time. */ + +static void fill_with_check(int start, int end, void *arg) +{ + int *a = (int *)arg; + for (int i = start; i < end; i++) + a[i] = i * 3 + 7; +} + +static int test_parallel_for_sustained(void) +{ + threadpool_t *pool = tp_create(8); + CHECK(pool != NULL); + + int N = 50000; + int *arr = (int *)calloc((size_t)N, sizeof(int)); + CHECK(arr != NULL); + + for (int iter = 0; iter < 100; iter++) { + memset(arr, 0, (size_t)N * sizeof(int)); + tp_parallel_for(pool, 0, N, fill_with_check, arr); + for (int i = 0; i < N; i++) + CHECK(arr[i] == i * 3 + 7); + } + + free(arr); + tp_destroy(pool); + return 0; +} + +/* ── 8. Single-element ranges and edge cases ─────────────────── */ + +static void count_calls(int start, int end, void *arg) +{ + atomic_int *c = (atomic_int *)arg; + for (int i = start; i < end; i++) + atomic_fetch_add(c, 1); +} + +static int test_edge_cases(void) +{ + threadpool_t *pool = tp_create(4); + CHECK(pool != NULL); + + /* Single element */ + atomic_int counter; + atomic_init(&counter, 0); + tp_parallel_for(pool, 0, 1, count_calls, &counter); + CHECK(atomic_load(&counter) == 1); + + /* Two elements */ + atomic_init(&counter, 0); + tp_parallel_for(pool, 0, 2, count_calls, &counter); + CHECK(atomic_load(&counter) == 2); + + /* Range smaller than thread count */ + atomic_init(&counter, 0); + tp_parallel_for(pool, 0, 3, count_calls, &counter); + CHECK(atomic_load(&counter) == 3); + + /* Range equal to thread count */ + atomic_init(&counter, 0); + tp_parallel_for(pool, 0, 4, count_calls, &counter); + CHECK(atomic_load(&counter) == 4); + + /* Negative/empty ranges */ + atomic_init(&counter, 0); + tp_parallel_for(pool, 5, 5, count_calls, &counter); + CHECK(atomic_load(&counter) == 0); + tp_parallel_for(pool, 10, 3, count_calls, &counter); + CHECK(atomic_load(&counter) == 0); + + /* NULL pool is UB, so we don't test it. */ + + tp_destroy(pool); + return 0; +} + +/* ── 9. Many sequential groups (lifecycle churn) ─────────────── */ + +static int test_group_lifecycle_churn(void) +{ + threadpool_t *pool = tp_create(4); + CHECK(pool != NULL); + + for (int i = 0; i < 10000; i++) { + atomic_int counter; + atomic_init(&counter, 0); + + tp_group_t *g = tp_group_create(pool); + CHECK(g != NULL); + tp_group_submit(g, atomic_inc, &counter); + tp_group_wait(g); + tp_group_destroy(g); + + CHECK(atomic_load(&counter) == 1); + } + + tp_destroy(pool); + return 0; +} + +/* ── 10. Deeply nested recursive tasks ───────────────────────── */ +/* Depth 20 = 1M leaves. LIFO pop bounds stack to O(20) per + * thread. Would overflow with a naive FIFO approach. */ + +struct deep_node { + struct deep_node *left; + struct deep_node *right; + int leaf_id; + int result; +}; + +static struct deep_node *build_deep_tree(int depth, int *next_id) +{ + struct deep_node *n = (struct deep_node *)calloc(1, sizeof(*n)); + if (!n) return NULL; + if (depth == 0) { + n->leaf_id = (*next_id)++; + return n; + } + n->leaf_id = -1; + n->left = build_deep_tree(depth - 1, next_id); + n->right = build_deep_tree(depth - 1, next_id); + return n; +} + +static void free_deep_tree(struct deep_node *n) +{ + if (!n) return; + free_deep_tree(n->left); + free_deep_tree(n->right); + free(n); +} + +struct deep_arg { + threadpool_t *pool; + struct deep_node *node; + int cutoff; +}; + +static int serial_deep_sum(struct deep_node *n) +{ + if (n->leaf_id >= 0) return 1; + return serial_deep_sum(n->left) + serial_deep_sum(n->right); +} + +static void deep_serial_task(struct deep_node *n) +{ + if (n->leaf_id >= 0) { n->result = 1; return; } + deep_serial_task(n->left); + deep_serial_task(n->right); + n->result = n->left->result + n->right->result; +} + +static void deep_task(void *arg) +{ + struct deep_arg *da = (struct deep_arg *)arg; + struct deep_node *n = da->node; + + if (n->leaf_id >= 0) { n->result = 1; return; } + + if (da->cutoff <= 0) { + /* Below cutoff: run serially to avoid task-creation overhead. */ + deep_serial_task(n); + return; + } + + struct deep_arg left = { da->pool, n->left, da->cutoff - 1 }; + struct deep_arg right = { da->pool, n->right, da->cutoff - 1 }; + + tp_group_t *g = tp_group_create(da->pool); + if (!g) { deep_serial_task(n); return; } + tp_group_submit(g, deep_task, &left); + tp_group_submit(g, deep_task, &right); + tp_group_wait(g); + tp_group_destroy(g); + + n->result = n->left->result + n->right->result; +} + +static int test_deep_tree_stress(void) +{ +#ifdef STRESS_TSAN + /* TSan inflates stack frames ~5x; reduce depth to avoid overflow + * during recursive tp_group_wait → execute_task → deep_task chains. */ + int depth = 14; + int cutoff = 5; +#else + int depth = 20; /* 1,048,576 leaves */ + int cutoff = 10; +#endif + int id = 0; + struct deep_node *root = build_deep_tree(depth, &id); + CHECK(root != NULL); + CHECK(id == (1 << depth)); + + int expected = serial_deep_sum(root); + CHECK(expected == (1 << depth)); + + threadpool_t *pool = tp_create(8); + CHECK(pool != NULL); + + struct deep_arg da = { pool, root, cutoff }; + deep_task(&da); + + CHECK(root->result == expected); + + tp_destroy(pool); + free_deep_tree(root); + return 0; +} + +/* ── main ─────────────────────────────────────────────────────── */ + +int main(void) +{ + printf("threadpool stress tests\n"); + printf("=======================\n\n"); + + double t0 = now_sec(); + + RUN(high_contention_ext_queue); + RUN(rapid_create_destroy); + RUN(deque_overflow_fallback); + RUN(oversubscription); + RUN(sustained_mixed_patterns); + RUN(shutdown_during_work); + RUN(parallel_for_sustained); + RUN(edge_cases); + RUN(group_lifecycle_churn); + RUN(deep_tree_stress); + + double elapsed = now_sec() - t0; + printf("\n%d/%d passed (%.1fs)", g_pass, g_total, elapsed); + if (g_fail > 0) printf(", %d FAILED", g_fail); + printf("\n"); + + return g_fail > 0 ? 1 : 0; +} diff --git a/lib/src/threadpool/test_threadpool.c b/lib/src/threadpool/test_threadpool.c new file mode 100644 index 0000000..08a7319 --- /dev/null +++ b/lib/src/threadpool/test_threadpool.c @@ -0,0 +1,579 @@ +/* + * test_threadpool.c — Tests for all three parallelism patterns. + * + * 1. Parallel for (distance-matrix style) + * 2. Fork-join (Hirschberg forward/backward) + * 3. Recursive tasks (guide-tree traversal) + */ + +#include "threadpool.h" +#include +#include +#include +#include + +/* ── Test harness ─────────────────────────────────────────────── */ + +static int g_pass, g_fail, g_total; + +#define CHECK(cond) do { \ + if (!(cond)) { \ + fprintf(stderr, " FAIL: %s (line %d)\n", \ + #cond, __LINE__); \ + return -1; \ + } \ +} while (0) + +#define RUN(name) do { \ + printf(" %-45s ", #name); \ + fflush(stdout); \ + g_total++; \ + if (test_##name() == 0) { printf("PASS\n"); g_pass++; } \ + else { printf("FAIL\n"); g_fail++; } \ +} while (0) + +/* ── 1. create / destroy ─────────────────────────────────────── */ + +static int test_create_destroy(void) +{ + threadpool_t *p = tp_create(2); + CHECK(p != NULL); + CHECK(tp_get_nthreads(p) == 2); + tp_destroy(p); + return 0; +} + +static int test_create_auto(void) +{ + threadpool_t *p = tp_create(0); + CHECK(p != NULL); + CHECK(tp_get_nthreads(p) >= 1); + tp_destroy(p); + return 0; +} + +/* ── 2. parallel for ─────────────────────────────────────────── */ + +static void fill_squares(int start, int end, void *arg) +{ + int *arr = (int *)arg; + for (int i = start; i < end; i++) + arr[i] = i * i; +} + +static int test_parallel_for_correctness(void) +{ + int N = 10000; + int *arr = (int *)calloc((size_t)N, sizeof(int)); + CHECK(arr != NULL); + + threadpool_t *pool = tp_create(4); + CHECK(pool != NULL); + + tp_parallel_for(pool, 0, N, fill_squares, arr); + + for (int i = 0; i < N; i++) + CHECK(arr[i] == i * i); + + tp_destroy(pool); + free(arr); + return 0; +} + +static int test_parallel_for_single_thread(void) +{ + int N = 5000; + int *arr = (int *)calloc((size_t)N, sizeof(int)); + CHECK(arr != NULL); + + threadpool_t *pool = tp_create(1); + tp_parallel_for(pool, 0, N, fill_squares, arr); + + for (int i = 0; i < N; i++) + CHECK(arr[i] == i * i); + + tp_destroy(pool); + free(arr); + return 0; +} + +static int test_parallel_for_empty_range(void) +{ + threadpool_t *pool = tp_create(2); + /* Should be a no-op, not crash. */ + tp_parallel_for(pool, 5, 5, fill_squares, NULL); + tp_parallel_for(pool, 10, 3, fill_squares, NULL); + tp_destroy(pool); + return 0; +} + +/* Distance-matrix style: outer loop parallel, inner loop serial. */ + +struct dm_ctx { + float **dm; + uint8_t **seqs; + int seq_len; + int num_anchors; +}; + +static void dm_chunk(int start, int end, void *arg) +{ + struct dm_ctx *ctx = (struct dm_ctx *)arg; + for (int i = start; i < end; i++) { + for (int j = 0; j < ctx->num_anchors; j++) { + int diff = 0; + for (int k = 0; k < ctx->seq_len; k++) + diff += (ctx->seqs[i][k] != ctx->seqs[j][k]); + ctx->dm[i][j] = (float)diff / (float)ctx->seq_len; + } + } +} + +static int test_parallel_for_distance_matrix(void) +{ + int N = 200, M = 20, L = 50; + + /* Allocate sequences */ + uint8_t **seqs = (uint8_t **)malloc((size_t)N * sizeof(uint8_t *)); + CHECK(seqs != NULL); + for (int i = 0; i < N; i++) { + seqs[i] = (uint8_t *)malloc((size_t)L); + for (int j = 0; j < L; j++) + seqs[i][j] = (uint8_t)((i * 7 + j * 13) % 20); + } + + /* Serial reference */ + float **ref = (float **)malloc((size_t)N * sizeof(float *)); + for (int i = 0; i < N; i++) { + ref[i] = (float *)calloc((size_t)M, sizeof(float)); + for (int j = 0; j < M; j++) { + int diff = 0; + for (int k = 0; k < L; k++) + diff += (seqs[i][k] != seqs[j][k]); + ref[i][j] = (float)diff / (float)L; + } + } + + /* Parallel computation */ + float **dm = (float **)malloc((size_t)N * sizeof(float *)); + for (int i = 0; i < N; i++) + dm[i] = (float *)calloc((size_t)M, sizeof(float)); + + struct dm_ctx ctx = { dm, seqs, L, M }; + threadpool_t *pool = tp_create(4); + tp_parallel_for(pool, 0, N, dm_chunk, &ctx); + + for (int i = 0; i < N; i++) + for (int j = 0; j < M; j++) + CHECK(dm[i][j] == ref[i][j]); + + tp_destroy(pool); + for (int i = 0; i < N; i++) { free(seqs[i]); free(ref[i]); free(dm[i]); } + free(seqs); free(ref); free(dm); + return 0; +} + +/* ── 3. fork-join ─────────────────────────────────────────────── */ + +struct fj_arg { + int *out; + int val; +}; + +static void fj_write(void *arg) +{ + struct fj_arg *a = (struct fj_arg *)arg; + *a->out = a->val; +} + +static int test_fork_join_two(void) +{ + threadpool_t *pool = tp_create(2); + int a = 0, b = 0; + struct fj_arg aa = { &a, 42 }; + struct fj_arg bb = { &b, 99 }; + + tp_group_t *g = tp_group_create(pool); + tp_group_submit(g, fj_write, &aa); + tp_group_submit(g, fj_write, &bb); + tp_group_wait(g); + tp_group_destroy(g); + + CHECK(a == 42); + CHECK(b == 99); + tp_destroy(pool); + return 0; +} + +static int test_fork_join_four(void) +{ + threadpool_t *pool = tp_create(4); + int results[4] = {0}; + struct fj_arg args[4]; + for (int i = 0; i < 4; i++) { + args[i].out = &results[i]; + args[i].val = (i + 1) * 10; + } + + tp_group_t *g = tp_group_create(pool); + for (int i = 0; i < 4; i++) + tp_group_submit(g, fj_write, &args[i]); + tp_group_wait(g); + tp_group_destroy(g); + + for (int i = 0; i < 4; i++) + CHECK(results[i] == (i + 1) * 10); + + tp_destroy(pool); + return 0; +} + +/* ── 4. recursive tasks (parallel fibonacci) ──────────────────── */ + +struct fib_arg { + threadpool_t *pool; + int n; + int result; +}; + +static void fib_task(void *arg) +{ + struct fib_arg *f = (struct fib_arg *)arg; + if (f->n <= 1) { + f->result = f->n; + return; + } + struct fib_arg left = { .pool = f->pool, .n = f->n - 1, .result = 0 }; + struct fib_arg right = { .pool = f->pool, .n = f->n - 2, .result = 0 }; + + tp_group_t *g = tp_group_create(f->pool); + tp_group_submit(g, fib_task, &left); + tp_group_submit(g, fib_task, &right); + tp_group_wait(g); + tp_group_destroy(g); + + f->result = left.result + right.result; +} + +static int serial_fib(int n) +{ + if (n <= 1) return n; + return serial_fib(n - 1) + serial_fib(n - 2); +} + +static int test_recursive_fib(void) +{ + threadpool_t *pool = tp_create(4); + + for (int n = 0; n <= 15; n++) { + struct fib_arg f = { .pool = pool, .n = n, .result = -1 }; + fib_task(&f); + CHECK(f.result == serial_fib(n)); + } + + tp_destroy(pool); + return 0; +} + +/* ── 5. recursive tree (simulates guide-tree traversal) ───────── */ + +struct tree_node { + struct tree_node *left; + struct tree_node *right; + int leaf_id; /* >= 0 for leaves, -1 for internal */ + float result; +}; + +static struct tree_node *build_tree(int depth, int *next_id) +{ + struct tree_node *n = (struct tree_node *)calloc(1, sizeof(*n)); + if (depth == 0) { + n->leaf_id = (*next_id)++; + return n; + } + n->leaf_id = -1; + n->left = build_tree(depth - 1, next_id); + n->right = build_tree(depth - 1, next_id); + return n; +} + +static void free_tree(struct tree_node *n) +{ + if (!n) return; + free_tree(n->left); + free_tree(n->right); + free(n); +} + +struct tree_task_arg { + threadpool_t *pool; + struct tree_node *node; +}; + +static float leaf_work(int id) +{ + float sum = 0.0f; + unsigned x = (unsigned)id * 2654435761u; + for (int i = 0; i < 100; i++) { + x = x * 1103515245u + 12345u; + sum += (float)(x >> 16) / 65536.0f; + } + return sum; +} + +static void tree_task(void *arg) +{ + struct tree_task_arg *ta = (struct tree_task_arg *)arg; + struct tree_node *n = ta->node; + + if (n->leaf_id >= 0) { + n->result = leaf_work(n->leaf_id); + return; + } + + struct tree_task_arg left_arg = { ta->pool, n->left }; + struct tree_task_arg right_arg = { ta->pool, n->right }; + + tp_group_t *g = tp_group_create(ta->pool); + tp_group_submit(g, tree_task, &left_arg); + tp_group_submit(g, tree_task, &right_arg); + tp_group_wait(g); + tp_group_destroy(g); + + n->result = n->left->result + n->right->result; +} + +static float serial_tree(struct tree_node *n) +{ + if (n->leaf_id >= 0) + return leaf_work(n->leaf_id); + return serial_tree(n->left) + serial_tree(n->right); +} + +static int test_recursive_tree(void) +{ + int id = 0; + struct tree_node *root = build_tree(10, &id); /* 1024 leaves */ + + float serial_result = serial_tree(root); + + /* Reset results */ + /* (serial_tree wrote into the same nodes — rebuild) */ + free_tree(root); + id = 0; + root = build_tree(10, &id); + + threadpool_t *pool = tp_create(4); + struct tree_task_arg targ = { pool, root }; + tree_task(&targ); + + /* Compare within float tolerance */ + float diff = root->result - serial_result; + if (diff < 0) diff = -diff; + CHECK(diff < 0.001f); + + tp_destroy(pool); + free_tree(root); + return 0; +} + +/* ── 6. many small tasks (stress test) ────────────────────────── */ + +static void increment(void *arg) +{ + atomic_int *counter = (atomic_int *)arg; + atomic_fetch_add(counter, 1); +} + +static int test_many_tasks(void) +{ + int N = 10000; + threadpool_t *pool = tp_create(4); + atomic_int counter; + atomic_init(&counter, 0); + + tp_group_t *g = tp_group_create(pool); + for (int i = 0; i < N; i++) + tp_group_submit(g, increment, &counter); + tp_group_wait(g); + tp_group_destroy(g); + + CHECK(atomic_load(&counter) == N); + tp_destroy(pool); + return 0; +} + +/* ── 7. nested groups (non-recursive) ─────────────────────────── */ + +struct nested_arg { + threadpool_t *pool; + atomic_int *counter; +}; + +static void inner_task(void *arg) +{ + atomic_int *c = (atomic_int *)arg; + atomic_fetch_add(c, 1); +} + +static void outer_task(void *arg) +{ + struct nested_arg *na = (struct nested_arg *)arg; + tp_group_t *g = tp_group_create(na->pool); + for (int i = 0; i < 5; i++) + tp_group_submit(g, inner_task, na->counter); + tp_group_wait(g); + tp_group_destroy(g); +} + +static int test_nested_groups(void) +{ + threadpool_t *pool = tp_create(4); + atomic_int counter; + atomic_init(&counter, 0); + + struct nested_arg na = { pool, &counter }; + + tp_group_t *g = tp_group_create(pool); + for (int i = 0; i < 10; i++) + tp_group_submit(g, outer_task, &na); + tp_group_wait(g); + tp_group_destroy(g); + + /* 10 outer * 5 inner = 50 */ + CHECK(atomic_load(&counter) == 50); + tp_destroy(pool); + return 0; +} + +/* ── 8. group wait with no tasks ──────────────────────────────── */ + +static int test_empty_group_wait(void) +{ + threadpool_t *pool = tp_create(2); + tp_group_t *g = tp_group_create(pool); + tp_group_wait(g); /* should return immediately */ + tp_group_destroy(g); + tp_destroy(pool); + return 0; +} + +/* ── 9. large parallel for ────────────────────────────────────── */ + +static int test_large_parallel_for(void) +{ + /* N must be ≤ 46340 to avoid signed int overflow in i*i + * (sqrt(INT_MAX) ≈ 46340). The compiler exploits overflow UB. */ + int N = 46340; + int *arr = (int *)calloc((size_t)N, sizeof(int)); + CHECK(arr != NULL); + + threadpool_t *pool = tp_create(8); + tp_parallel_for(pool, 0, N, fill_squares, arr); + + for (int i = 0; i < N; i++) + CHECK(arr[i] == i * i); + + tp_destroy(pool); + free(arr); + return 0; +} + +/* ── 10. deep recursive tree (V2 stack-safety proof) ──────────── */ + +/* Depth 16 = 65536 leaves. V1 (global FIFO queue) would need + * O(65536) stack frames in the worst case, overflowing 8 MB. + * V2 (Chase-Lev LIFO) needs O(16) frames per thread. */ +static int test_deep_recursive_tree(void) +{ + int id = 0; + struct tree_node *root = build_tree(16, &id); /* 65536 leaves */ + + /* Serial reference */ + float serial_result = serial_tree(root); + free_tree(root); + id = 0; + root = build_tree(16, &id); + + threadpool_t *pool = tp_create(4); + struct tree_task_arg targ = { pool, root }; + tree_task(&targ); + + float diff = root->result - serial_result; + if (diff < 0) diff = -diff; + CHECK(diff < 0.01f); + + tp_destroy(pool); + free_tree(root); + return 0; +} + +/* ── 11. deep recursive fibonacci ─────────────────────────────── */ + +static int test_deep_recursive_fib(void) +{ + threadpool_t *pool = tp_create(4); + + /* fib(20) = 6765 — creates ~13K groups, ~26K tasks. */ + struct fib_arg f = { .pool = pool, .n = 20, .result = -1 }; + fib_task(&f); + CHECK(f.result == 6765); + + tp_destroy(pool); + return 0; +} + +/* ── 12. multiple sequential groups ───────────────────────────── */ + +static int test_sequential_groups(void) +{ + threadpool_t *pool = tp_create(4); + + for (int round = 0; round < 20; round++) { + atomic_int counter; + atomic_init(&counter, 0); + + tp_group_t *g = tp_group_create(pool); + for (int i = 0; i < 100; i++) + tp_group_submit(g, increment, &counter); + tp_group_wait(g); + tp_group_destroy(g); + + CHECK(atomic_load(&counter) == 100); + } + + tp_destroy(pool); + return 0; +} + +/* ── main ─────────────────────────────────────────────────────── */ + +int main(void) +{ + printf("threadpool tests\n"); + printf("================\n\n"); + + RUN(create_destroy); + RUN(create_auto); + RUN(parallel_for_correctness); + RUN(parallel_for_single_thread); + RUN(parallel_for_empty_range); + RUN(parallel_for_distance_matrix); + RUN(fork_join_two); + RUN(fork_join_four); + RUN(recursive_fib); + RUN(recursive_tree); + RUN(many_tasks); + RUN(nested_groups); + RUN(empty_group_wait); + RUN(large_parallel_for); + RUN(deep_recursive_tree); + RUN(deep_recursive_fib); + RUN(sequential_groups); + + printf("\n%d/%d passed", g_pass, g_total); + if (g_fail > 0) printf(", %d FAILED", g_fail); + printf("\n"); + + return g_fail > 0 ? 1 : 0; +} diff --git a/lib/src/threadpool/threadpool.c b/lib/src/threadpool/threadpool.c new file mode 100644 index 0000000..f998c47 --- /dev/null +++ b/lib/src/threadpool/threadpool.c @@ -0,0 +1,709 @@ +/* + * threadpool.c — Chase-Lev work-stealing thread pool. + * + * See threadpool.h for the public API and usage examples. + * + * Internal architecture: + * + * Queues: + * - Per-worker Chase-Lev deques (lock-free, LIFO pop, FIFO steal). + * - Global "external" queue (mutex-protected) for non-worker submissions. + * + * Work-finding priority (per worker): + * 1. Own deque (LIFO) — preserves DFS order, bounds stack depth. + * 2. External queue — picks up tasks submitted from non-workers. + * 3. Steal from peer — random victim, FIFO steals shallowest work. + * + * Sleeping: + * Event-count protocol (wake_gen + condvar). Workers spin briefly, + * then park. Submitters bump wake_gen and signal if anyone sleeps. + * + * Group recycling: + * Per-worker free lists for tp_group_t objects. Groups are 16 bytes + * and follow strict LIFO create/destroy ordering, so thread-local + * recycling eliminates virtually all malloc/free after warmup. + * + * Stack depth: + * LIFO pop means a recursive tree traversal nests O(tree_depth) wait + * frames per thread, not O(tree_size). For a balanced tree of 1M + * leaves (depth ~20), each frame is ~500 bytes → ~10 KB total. + * + * Ref: Chase & Lev, "Dynamic Circular Work-Stealing Deque", SPAA 2005. + */ + +#include "threadpool.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ── TSan annotations for Chase-Lev deque ──────────────────────── + * TSan can't track happens-before through atomic_thread_fence, + * so we annotate the deque push/pop/steal with explicit acquire/ + * release on the buffer slot. Zero cost when TSan is off. */ +#if defined(__SANITIZE_THREAD__) +#define TP_TSAN 1 +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) +#define TP_TSAN 1 +#endif +#endif + +#ifdef TP_TSAN +void __tsan_acquire(void *addr); +void __tsan_release(void *addr); +#define TSAN_RELEASE(addr) __tsan_release(addr) +#define TSAN_ACQUIRE(addr) __tsan_acquire(addr) +#else +#define TSAN_RELEASE(addr) ((void)0) +#define TSAN_ACQUIRE(addr) ((void)0) +#endif + +/* Abort on pthread errors that indicate programming bugs (EINVAL, + * EDEADLK, etc.). These never fail in correct code, but catching + * them immediately beats silent corruption. */ +#define PTHREAD_CHECK(call) do { \ + int _rc = (call); \ + if (_rc != 0) { \ + fprintf(stderr, "threadpool: %s failed (%d)\n", #call, _rc); \ + abort(); \ + } \ +} while (0) + +/* ── Internal task ────────────────────────────────────────────── */ + +typedef struct { + void (*fn)(void *); + void *arg; + atomic_int *pending; /* &group->pending, or NULL */ +} tp_task_t; + +/* ══════════════════════════════════════════════════════════════════ + * Chase-Lev work-stealing deque + * + * Owner pushes/pops at bottom (LIFO). Thieves steal from top (FIFO). + * Lock-free: owner ops use fences, steal uses CAS on top. + * Fixed capacity (power of 2). + * ══════════════════════════════════════════════════════════════════ */ + +#define DEQUE_DEFAULT_CAP 4096 /* per worker; holds O(tree_depth) tasks */ + +typedef struct { + tp_task_t *buf; + long cap; /* power of 2 */ + atomic_long bottom; /* modified by owner */ + char _pad[64]; /* keep bottom and top on separate cache lines */ + atomic_long top; /* CAS'd by thieves */ +} ws_deque_t; + +static int deque_init(ws_deque_t *dq, long cap) +{ + dq->buf = (tp_task_t *)calloc((size_t)cap, sizeof(tp_task_t)); + if (!dq->buf) return -1; + dq->cap = cap; + atomic_store_explicit(&dq->bottom, 0, memory_order_relaxed); + atomic_store_explicit(&dq->top, 0, memory_order_relaxed); + return 0; +} + +static void deque_destroy(ws_deque_t *dq) +{ + free(dq->buf); +} + +/* Owner push. Returns 0 on success, -1 if full. */ +static int deque_push(ws_deque_t *dq, tp_task_t task) +{ + long b = atomic_load_explicit(&dq->bottom, memory_order_relaxed); + long t = atomic_load_explicit(&dq->top, memory_order_acquire); + if (b - t >= dq->cap) + return -1; /* full */ + long slot = b & (dq->cap - 1); + dq->buf[slot] = task; + TSAN_RELEASE(&dq->buf[slot]); + atomic_thread_fence(memory_order_release); /* task visible before bottom bumps */ + atomic_store_explicit(&dq->bottom, b + 1, memory_order_relaxed); + return 0; +} + +/* Owner pop (LIFO). Returns 0 on success, -1 if empty. */ +static int deque_pop(ws_deque_t *dq, tp_task_t *out) +{ + long b = atomic_load_explicit(&dq->bottom, memory_order_relaxed) - 1; + atomic_store_explicit(&dq->bottom, b, memory_order_relaxed); + atomic_thread_fence(memory_order_seq_cst); + long t = atomic_load_explicit(&dq->top, memory_order_relaxed); + + if (t <= b) { + long slot = b & (dq->cap - 1); + *out = dq->buf[slot]; + TSAN_ACQUIRE(&dq->buf[slot]); + if (t == b) { + /* Last element — race with a stealer. */ + if (!atomic_compare_exchange_strong_explicit( + &dq->top, &t, t + 1, + memory_order_seq_cst, memory_order_relaxed)) { + /* Stealer won. */ + atomic_store_explicit(&dq->bottom, b + 1, memory_order_relaxed); + return -1; + } + atomic_store_explicit(&dq->bottom, b + 1, memory_order_relaxed); + } + return 0; + } + /* Was already empty. */ + atomic_store_explicit(&dq->bottom, b + 1, memory_order_relaxed); + return -1; +} + +/* Thief steal (FIFO). Returns 0 on success, -1 if empty or contention. */ +static int deque_steal(ws_deque_t *dq, tp_task_t *out) +{ + long t = atomic_load_explicit(&dq->top, memory_order_acquire); + atomic_thread_fence(memory_order_seq_cst); + long b = atomic_load_explicit(&dq->bottom, memory_order_acquire); + + if (t < b) { + long slot = t & (dq->cap - 1); + *out = dq->buf[slot]; + if (!atomic_compare_exchange_strong_explicit( + &dq->top, &t, t + 1, + memory_order_seq_cst, memory_order_relaxed)) + return -1; /* another stealer won */ + TSAN_ACQUIRE(&dq->buf[slot]); + return 0; + } + return -1; /* empty */ +} + +/* ══════════════════════════════════════════════════════════════════ + * External queue — mutex-protected, for non-worker submissions + * ══════════════════════════════════════════════════════════════════ */ + +typedef struct { + tp_task_t *buf; + int cap; + int head; + int tail; + int count; + pthread_mutex_t lock; +} ext_queue_t; + +static int ext_init(ext_queue_t *q, int cap) +{ + q->buf = (tp_task_t *)calloc((size_t)cap, sizeof(tp_task_t)); + if (!q->buf) return -1; + q->cap = cap; + q->head = q->tail = q->count = 0; + if (pthread_mutex_init(&q->lock, NULL) != 0) { + free(q->buf); + q->buf = NULL; + return -1; + } + return 0; +} + +static void ext_destroy(ext_queue_t *q) +{ + free(q->buf); + PTHREAD_CHECK(pthread_mutex_destroy(&q->lock)); +} + +/* Caller must hold q->lock. */ +static int ext_grow_locked(ext_queue_t *q) +{ + if (q->cap > INT_MAX / 2) return -1; /* overflow guard */ + int new_cap = q->cap * 2; + tp_task_t *nb = (tp_task_t *)calloc((size_t)new_cap, sizeof(tp_task_t)); + if (!nb) return -1; + for (int i = 0; i < q->count; i++) + nb[i] = q->buf[(q->head + i) % q->cap]; + free(q->buf); + q->buf = nb; + q->head = 0; + q->tail = q->count; + q->cap = new_cap; + return 0; +} + +static int ext_push(ext_queue_t *q, tp_task_t task) +{ + PTHREAD_CHECK(pthread_mutex_lock(&q->lock)); + if (q->count == q->cap && ext_grow_locked(q) != 0) { + PTHREAD_CHECK(pthread_mutex_unlock(&q->lock)); + return -1; + } + q->buf[q->tail] = task; + q->tail = (q->tail + 1) % q->cap; + q->count++; + PTHREAD_CHECK(pthread_mutex_unlock(&q->lock)); + return 0; +} + +static int ext_try_pop(ext_queue_t *q, tp_task_t *out) +{ + PTHREAD_CHECK(pthread_mutex_lock(&q->lock)); + if (q->count == 0) { + PTHREAD_CHECK(pthread_mutex_unlock(&q->lock)); + return -1; + } + *out = q->buf[q->head]; + q->head = (q->head + 1) % q->cap; + q->count--; + PTHREAD_CHECK(pthread_mutex_unlock(&q->lock)); + return 0; +} + +/* ══════════════════════════════════════════════════════════════════ + * Pool, worker, group structures + * ══════════════════════════════════════════════════════════════════ */ + +#define MAX_FREE_GROUPS 64 + +typedef struct worker { + ws_deque_t deque; + struct threadpool *pool; + pthread_t thread; + int id; + unsigned rng; /* xorshift state for random steal */ + struct tp_group *free_groups; /* per-worker group free list */ + int n_free; +} worker_t; + +struct threadpool { + worker_t *workers; + int nworkers; + ext_queue_t ext; + + atomic_int wake_gen; /* event count for wakeup */ + atomic_int n_sleeping; + pthread_mutex_t wake_lock; + pthread_cond_t wake_cond; + + atomic_int shutdown; +}; + +struct tp_group { + union { + threadpool_t *pool; /* valid when active */ + struct tp_group *next; /* valid when on free list */ + }; + atomic_int pending; +}; + +/* ── Thread-local: current worker (NULL on non-worker threads) ── */ +static _Thread_local worker_t *tl_worker = NULL; + +/* ── RNG for steal-target selection ───────────────────────────── */ + +static unsigned xorshift32(unsigned *s) +{ + unsigned x = *s; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return *s = x; +} + +/* ── Task execution ───────────────────────────────────────────── */ + +static void execute_task(tp_task_t *t) +{ + t->fn(t->arg); + if (t->pending) + atomic_fetch_sub(t->pending, 1); +} + +/* ── Worker wakeup ────────────────────────────────────────────── */ + +static void notify_workers(threadpool_t *pool) +{ + atomic_fetch_add_explicit(&pool->wake_gen, 1, memory_order_release); + if (atomic_load_explicit(&pool->n_sleeping, memory_order_relaxed) > 0) { + PTHREAD_CHECK(pthread_mutex_lock(&pool->wake_lock)); + PTHREAD_CHECK(pthread_cond_signal(&pool->wake_cond)); + PTHREAD_CHECK(pthread_mutex_unlock(&pool->wake_lock)); + } +} + +/* ── Work-finding protocols ───────────────────────────────────── */ + +/* Called by worker threads: own deque (LIFO) → ext queue → steal. */ +static int try_find_work(worker_t *w, tp_task_t *out) +{ + /* 1. Own deque — LIFO preserves DFS order, bounds stack depth. */ + if (deque_pop(&w->deque, out) == 0) return 0; + + /* 2. External queue — picks up tasks from non-worker submitters. */ + if (ext_try_pop(&w->pool->ext, out) == 0) return 0; + + /* 3. Steal from a random peer — FIFO takes shallowest work. */ + int n = w->pool->nworkers; + if (n > 1) { + int start = (int)(xorshift32(&w->rng) % (unsigned)n); + for (int i = 0; i < n; i++) { + int v = (start + i) % n; + if (v == w->id) continue; + if (deque_steal(&w->pool->workers[v].deque, out) == 0) + return 0; + } + } + return -1; +} + +/* Called by non-worker threads (main thread in tp_group_wait). */ +static int try_find_work_ext(threadpool_t *pool, tp_task_t *out, + unsigned *rng) +{ + if (ext_try_pop(&pool->ext, out) == 0) return 0; + + int n = pool->nworkers; + int start = (int)(xorshift32(rng) % (unsigned)n); + for (int i = 0; i < n; i++) { + int v = (start + i) % n; + if (deque_steal(&pool->workers[v].deque, out) == 0) + return 0; + } + return -1; +} + +/* ── Worker thread ────────────────────────────────────────────── */ + +static void *worker_main(void *arg) +{ + worker_t *w = (worker_t *)arg; + tl_worker = w; + threadpool_t *pool = w->pool; + tp_task_t task; + unsigned spins = 0; + + for (;;) { + if (try_find_work(w, &task) == 0) { + execute_task(&task); + spins = 0; + continue; + } + + if (atomic_load(&pool->shutdown)) + break; + + if (++spins < 64) + continue; /* brief spin before parking */ + + /* Park until new work arrives (event-count protocol). */ + int gen = atomic_load_explicit(&pool->wake_gen, + memory_order_acquire); + atomic_fetch_add(&pool->n_sleeping, 1); + PTHREAD_CHECK(pthread_mutex_lock(&pool->wake_lock)); + + while (atomic_load_explicit(&pool->wake_gen, + memory_order_acquire) == gen + && !atomic_load(&pool->shutdown)) + PTHREAD_CHECK(pthread_cond_wait(&pool->wake_cond, + &pool->wake_lock)); + + PTHREAD_CHECK(pthread_mutex_unlock(&pool->wake_lock)); + atomic_fetch_sub(&pool->n_sleeping, 1); + spins = 0; + } + return NULL; +} + +/* ══════════════════════════════════════════════════════════════════ + * Public API: pool lifecycle + * ══════════════════════════════════════════════════════════════════ */ + +threadpool_t *tp_create(int nthreads) +{ + if (nthreads <= 0) { + nthreads = (int)sysconf(_SC_NPROCESSORS_ONLN); + if (nthreads <= 0) nthreads = 4; + } + + threadpool_t *pool = (threadpool_t *)calloc(1, sizeof(*pool)); + if (!pool) return NULL; + + pool->nworkers = nthreads; + atomic_store_explicit(&pool->shutdown, 0, memory_order_relaxed); + atomic_store_explicit(&pool->wake_gen, 0, memory_order_relaxed); + atomic_store_explicit(&pool->n_sleeping, 0, memory_order_relaxed); + + if (pthread_mutex_init(&pool->wake_lock, NULL) != 0) { + free(pool); + return NULL; + } + if (pthread_cond_init(&pool->wake_cond, NULL) != 0) { + PTHREAD_CHECK(pthread_mutex_destroy(&pool->wake_lock)); + free(pool); + return NULL; + } + + if (ext_init(&pool->ext, 1024) != 0) { + PTHREAD_CHECK(pthread_cond_destroy(&pool->wake_cond)); + PTHREAD_CHECK(pthread_mutex_destroy(&pool->wake_lock)); + free(pool); + return NULL; + } + + pool->workers = (worker_t *)calloc((size_t)nthreads, sizeof(worker_t)); + if (!pool->workers) { + ext_destroy(&pool->ext); + PTHREAD_CHECK(pthread_cond_destroy(&pool->wake_cond)); + PTHREAD_CHECK(pthread_mutex_destroy(&pool->wake_lock)); + free(pool); + return NULL; + } + + for (int i = 0; i < nthreads; i++) { + worker_t *w = &pool->workers[i]; + if (deque_init(&w->deque, DEQUE_DEFAULT_CAP) != 0) { + for (int j = 0; j < i; j++) + deque_destroy(&pool->workers[j].deque); + free(pool->workers); + ext_destroy(&pool->ext); + PTHREAD_CHECK(pthread_cond_destroy(&pool->wake_cond)); + PTHREAD_CHECK(pthread_mutex_destroy(&pool->wake_lock)); + free(pool); + return NULL; + } + w->pool = pool; + w->id = i; + w->rng = (unsigned)(i + 1) * 2654435761u; + } + + /* Default stacks are fine: LIFO pop bounds stack depth to + * O(tree_depth), which is a few KB at most. */ + for (int i = 0; i < nthreads; i++) { + if (pthread_create(&pool->workers[i].thread, NULL, + worker_main, &pool->workers[i]) != 0) { + atomic_store(&pool->shutdown, 1); + PTHREAD_CHECK(pthread_mutex_lock(&pool->wake_lock)); + PTHREAD_CHECK(pthread_cond_broadcast(&pool->wake_cond)); + PTHREAD_CHECK(pthread_mutex_unlock(&pool->wake_lock)); + for (int j = 0; j < i; j++) + PTHREAD_CHECK(pthread_join(pool->workers[j].thread, NULL)); + for (int j = 0; j < nthreads; j++) + deque_destroy(&pool->workers[j].deque); + free(pool->workers); + ext_destroy(&pool->ext); + PTHREAD_CHECK(pthread_mutex_destroy(&pool->wake_lock)); + PTHREAD_CHECK(pthread_cond_destroy(&pool->wake_cond)); + free(pool); + return NULL; + } + } + + return pool; +} + +void tp_destroy(threadpool_t *pool) +{ + if (!pool) return; + + atomic_store(&pool->shutdown, 1); + PTHREAD_CHECK(pthread_mutex_lock(&pool->wake_lock)); + PTHREAD_CHECK(pthread_cond_broadcast(&pool->wake_cond)); + PTHREAD_CHECK(pthread_mutex_unlock(&pool->wake_lock)); + + for (int i = 0; i < pool->nworkers; i++) + PTHREAD_CHECK(pthread_join(pool->workers[i].thread, NULL)); + + for (int i = 0; i < pool->nworkers; i++) { + /* Drain per-worker group free lists. */ + tp_group_t *g = pool->workers[i].free_groups; + while (g) { + tp_group_t *next = g->next; + free(g); + g = next; + } + deque_destroy(&pool->workers[i].deque); + } + free(pool->workers); + ext_destroy(&pool->ext); + PTHREAD_CHECK(pthread_mutex_destroy(&pool->wake_lock)); + PTHREAD_CHECK(pthread_cond_destroy(&pool->wake_cond)); + free(pool); +} + +int tp_get_nthreads(threadpool_t *pool) +{ + return pool ? pool->nworkers : 0; +} + +void tp_request_shutdown(threadpool_t *pool) +{ + if (!pool) return; + /* Only atomic store — safe to call from a signal handler. */ + atomic_store(&pool->shutdown, 1); +} + +/* ══════════════════════════════════════════════════════════════════ + * Public API: task groups + * ══════════════════════════════════════════════════════════════════ */ + +tp_group_t *tp_group_create(threadpool_t *pool) +{ + tp_group_t *g = NULL; + worker_t *w = tl_worker; + + /* Try per-worker free list — zero contention. */ + if (w && w->pool == pool && w->free_groups) { + g = w->free_groups; + w->free_groups = g->next; + w->n_free--; + } + + if (!g) { + g = (tp_group_t *)calloc(1, sizeof(*g)); + if (!g) return NULL; + } + + g->pool = pool; + atomic_store_explicit(&g->pending, 0, memory_order_relaxed); + return g; +} + +int tp_group_submit(tp_group_t *group, void (*fn)(void *), void *arg) +{ + atomic_fetch_add(&group->pending, 1); + tp_task_t task = { .fn = fn, .arg = arg, .pending = &group->pending }; + + worker_t *w = tl_worker; + int pushed = -1; + + if (w && w->pool == group->pool) + pushed = deque_push(&w->deque, task); + + if (pushed != 0) { + /* Non-worker or deque full — fall back to global queue. */ + if (ext_push(&group->pool->ext, task) != 0) { + atomic_fetch_sub(&group->pending, 1); + return -1; + } + } + + notify_workers(group->pool); + return 0; +} + +void tp_group_wait(tp_group_t *group) +{ + threadpool_t *pool = group->pool; + worker_t *w = tl_worker; + unsigned ext_rng = (unsigned)(uintptr_t)group * 2654435761u; + if (ext_rng == 0) ext_rng = 1; + unsigned spins = 0; + + while (atomic_load(&group->pending) > 0) { + if (atomic_load_explicit(&pool->shutdown, memory_order_relaxed)) + break; + + tp_task_t task; + int found; + + if (w && w->pool == pool) + found = (try_find_work(w, &task) == 0); + else + found = (try_find_work_ext(pool, &task, &ext_rng) == 0); + + if (found) { + execute_task(&task); + spins = 0; + } else if (++spins < 128) { + /* spin */ + } else { + sched_yield(); + spins = 0; + } + } +} + +void tp_group_destroy(tp_group_t *group) +{ + worker_t *w = tl_worker; + + if (w && w->pool == group->pool && w->n_free < MAX_FREE_GROUPS) { + group->next = w->free_groups; + w->free_groups = group; + w->n_free++; + } else { + free(group); + } +} + +/* ══════════════════════════════════════════════════════════════════ + * Public API: parallel for + * ══════════════════════════════════════════════════════════════════ */ + +struct pfor_chunk { + void (*fn)(int, int, void *); + void *arg; + int start; + int end; +}; + +static void pfor_worker(void *arg) +{ + struct pfor_chunk *c = (struct pfor_chunk *)arg; + c->fn(c->start, c->end, c->arg); +} + +#define MAX_STACK_CHUNKS 64 + +void tp_parallel_for(threadpool_t *pool, int start, int end, + void (*fn)(int, int, void *), void *arg) +{ + if (start >= end) return; + + int n = end - start; + int nchunks = pool->nworkers + 1; + if (nchunks > n) nchunks = n; + + struct pfor_chunk stack_chunks[MAX_STACK_CHUNKS]; + struct pfor_chunk *chunks = stack_chunks; + int heap = 0; + + if (nchunks > MAX_STACK_CHUNKS) { + chunks = (struct pfor_chunk *)calloc((size_t)nchunks, + sizeof(struct pfor_chunk)); + if (!chunks) { fn(start, end, arg); return; } + heap = 1; + } + + tp_group_t *group = tp_group_create(pool); + if (!group) { + if (heap) free(chunks); + fn(start, end, arg); + return; + } + + int chunk_size = (n + nchunks - 1) / nchunks; + int serial_from = end; /* if submit fails, run remaining chunks serially */ + for (int i = 0; i < nchunks; i++) { + int cs = start + i * chunk_size; + int ce = cs + chunk_size; + if (ce > end) ce = end; + if (cs >= end) break; + chunks[i].fn = fn; + chunks[i].arg = arg; + chunks[i].start = cs; + chunks[i].end = ce; + if (tp_group_submit(group, pfor_worker, &chunks[i]) != 0) { + serial_from = cs; + break; + } + } + + tp_group_wait(group); + tp_group_destroy(group); + + /* Execute any chunks that failed to submit. */ + if (serial_from < end) + fn(serial_from, end, arg); + + if (heap) free(chunks); +} diff --git a/lib/src/threadpool/threadpool.h b/lib/src/threadpool/threadpool.h new file mode 100644 index 0000000..d150a50 --- /dev/null +++ b/lib/src/threadpool/threadpool.h @@ -0,0 +1,139 @@ +/* + * threadpool.h — Lightweight work-stealing thread pool. + * + * A POSIX-threads-based thread pool using Chase-Lev work-stealing deques. + * Designed for recursive divide-and-conquer workloads (guide-tree traversal, + * Hirschberg alignment) and data-parallel loops (distance matrices). + * + * Supports three parallelism patterns: + * + * 1. PARALLEL FOR — tp_parallel_for() + * Splits [start, end) into chunks, one per worker. The calling thread + * participates. Best for embarrassingly parallel loops. + * + * 2. FORK-JOIN — tp_group_submit() + tp_group_wait() + * Submit N independent tasks, then block until all complete. The + * waiting thread executes queued work while it waits. + * + * 3. RECURSIVE TASKS — groups created inside tasks (nested wait) + * A task can create a new group, submit children, and wait. LIFO + * deque ordering ensures the C stack depth is O(tree_depth), not + * O(tree_size). Safe for trees with millions of nodes. + * + * LIFECYCLE: + * threadpool_t *pool = tp_create(0); // 0 = auto-detect CPUs + * // ... use pool for parallel work ... + * tp_destroy(pool); // joins all workers, frees all memory + * + * THREAD SAFETY: + * - tp_create / tp_destroy: call from one thread only. + * - tp_group_*: a group must be used by one owner thread at a time. + * The owner creates it, submits tasks, waits, and destroys it. + * Tasks submitted to the group may run on any worker thread. + * - tp_parallel_for: safe to call from any thread, including from + * inside a task (nested parallelism). + * - tp_request_shutdown: safe to call from any thread or signal handler. + * + * ERROR HANDLING: + * - tp_create returns NULL on failure (out of memory, pthread errors). + * - tp_group_create returns NULL on out of memory. + * - tp_group_submit returns -1 on failure (out of memory); the task + * is NOT executed. Check the return value. + * - tp_parallel_for never fails: it falls back to serial execution + * if allocation or submission fails. + * + * GRACEFUL SHUTDOWN (signal handling): + * Call tp_request_shutdown(pool) to make tp_group_wait() return early. + * This function is async-signal-safe (it only writes an atomic flag). + * After shutdown, call tp_destroy(pool) from the main thread. + * + * USAGE EXAMPLES: + * + * // Pattern 1: Parallel for (distance matrix) + * void compute_row(int start, int end, void *arg) { + * float **dm = (float **)arg; + * for (int i = start; i < end; i++) + * dm[i] = compute_distances(i); + * } + * tp_parallel_for(pool, 0, num_seqs, compute_row, dm); + * + * // Pattern 2: Fork-join (two independent tasks) + * tp_group_t *g = tp_group_create(pool); + * tp_group_submit(g, forward_pass, &fwd_arg); + * tp_group_submit(g, backward_pass, &bwd_arg); + * tp_group_wait(g); + * tp_group_destroy(g); + * + * // Pattern 3: Recursive tasks (guide-tree traversal) + * void align_node(void *arg) { + * struct node *n = (struct node *)arg; + * if (is_leaf(n)) { align_leaf(n); return; } + * tp_group_t *g = tp_group_create(pool); + * tp_group_submit(g, align_node, n->left); + * tp_group_submit(g, align_node, n->right); + * tp_group_wait(g); + * tp_group_destroy(g); + * merge_alignments(n); + * } + */ + +#ifndef THREADPOOL_H +#define THREADPOOL_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct threadpool threadpool_t; +typedef struct tp_group tp_group_t; + +/* ── Pool lifecycle ─────────────────────────────────────────────── */ + +/* Create a pool with nthreads workers (0 = auto-detect CPU count). + * Returns NULL on failure. */ +threadpool_t *tp_create(int nthreads); + +/* Destroy the pool. Joins all worker threads and frees all memory. + * All task groups MUST have been waited on and destroyed first. */ +void tp_destroy(threadpool_t *pool); + +/* Request shutdown without blocking. Causes tp_group_wait() to + * return early and workers to exit. Follow with tp_destroy(). + * Async-signal-safe (writes an atomic flag only). */ +void tp_request_shutdown(threadpool_t *pool); + +/* Number of worker threads in the pool. */ +int tp_get_nthreads(threadpool_t *pool); + +/* ── Task groups ────────────────────────────────────────────────── */ + +/* Create a group for coordinating related tasks. + * Returns NULL on out of memory. */ +tp_group_t *tp_group_create(threadpool_t *pool); + +/* Submit fn(arg) to the pool under this group. + * Returns 0 on success, -1 on failure (task NOT executed). + * May be called from any thread, including from inside a task. */ +int tp_group_submit(tp_group_t *group, void (*fn)(void *), void *arg); + +/* Block until every task in the group has completed. + * The calling thread participates in executing queued work while waiting. + * Returns early if tp_request_shutdown() has been called. */ +void tp_group_wait(tp_group_t *group); + +/* Free a group. Must be called after tp_group_wait(). */ +void tp_group_destroy(tp_group_t *group); + +/* ── Parallel for ───────────────────────────────────────────────── */ + +/* Partition [start, end) into chunks and call fn(chunk_start, chunk_end, arg) + * for each chunk. The calling thread participates in the work. + * Falls back to serial execution on allocation failure (never fails). */ +void tp_parallel_for(threadpool_t *pool, int start, int end, + void (*fn)(int start, int end, void *arg), void *arg); + +#ifdef __cplusplus +} +#endif + +#endif /* THREADPOOL_H */ From 3e84b9ab9408811e15e4d61e8e7797e7b58ee953 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Tue, 24 Mar 2026 14:31:24 +0800 Subject: [PATCH 07/29] Threadpool as default parallelization, unified CLI mode presets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace OpenMP with a built-in Chase-Lev work-stealing thread pool as the default parallelization backend. Falls back to serial if pthreads is unavailable. OpenMP remains available via -DUSE_OPENMP=ON. Threadpool is 1.5-2.5x faster than OpenMP across protein and nucleotide benchmarks on both ARM (M3) and x86-64 (Threadripper) hardware. Key changes: - Threadpool ON / OpenMP OFF by default in CMakeLists.txt - All parallel regions (distance matrix, k-means, Hirschberg, anchor selection, pairwise distances) wired for both backends - tp_parallel_for_chunked() with configurable minimum chunk size - Compile-time parallelization thresholds in one place (CMakeLists.txt): ALN_SERIAL_THRESHOLD=500, KMEANS_UPGMA_THRESHOLD=50, DIST_MIN_SEQS=50, PFOR_MIN_CHUNK=10 - macOS Python wheels use threadpool (no libomp.dylib dependency) CLI unified with Python API — both use NSGA-III optimized mode presets via kalign_get_mode_preset() + kalign_align_full(): kalign --mode fast|default|recall|accurate Gap penalty overrides (--gpo/--gpe/--tgpe) work with all modes. Removed dead CLI options (--ensemble, --refine, --consistency, etc.) that are now managed by mode presets. All three entry points (C CLI, Python API, kalign-py CLI) expose identical options and produce identical results. Bug fixes: - bisectingKmeans: base-case guard for num_samples <= 1 - bisectingKmeans: out-of-bounds seed_idx in split2 k-means loop - threadpool.c: missing #include for uintptr_t Co-Authored-By: Claude Opus 4.6 (1M context) --- .containerignore | 4 +- .github/workflows/wheels.yml | 8 +- CMakeLists.txt | 32 ++- Containerfile | 17 +- README.md | 38 +-- benchmarks/bench_modes.py | 63 ----- benchmarks/bench_quality_timing.py | 142 +++++++++++ benchmarks/run_balibase_comparison.py | 77 ++++++ lib/CMakeLists.txt | 25 +- lib/include/kalign/kalign.h | 3 + lib/src/aln_apair_dist.c | 35 +++ lib/src/aln_controller.c | 52 +++- lib/src/aln_mem.c | 3 + lib/src/aln_param.h | 10 +- lib/src/aln_run.c | 38 +++ lib/src/aln_struct.h | 7 + lib/src/aln_wrap.c | 77 +++++- lib/src/bisectingKmeans.c | 115 +++++++-- lib/src/msa_alloc.c | 1 - lib/src/msa_op.c | 9 + lib/src/msa_struct.h | 7 + lib/src/pick_anchor.c | 59 +++++ lib/src/sequence_distance.c | 51 +++- lib/src/threadpool/threadpool.c | 19 +- lib/src/threadpool/threadpool.h | 8 + pyproject.toml | 7 +- python-kalign/__init__.py | 6 +- python-kalign/_core.cpp | 7 +- python-kalign/cli.py | 2 +- src/parameters.c | 33 +-- src/parameters.h | 33 +-- src/run_kalign.c | 256 ++++--------------- tests/python/test_ecosystem_integration.py | 5 +- uv.lock | 277 +++++++++++++++++++++ 34 files changed, 1114 insertions(+), 412 deletions(-) delete mode 100644 benchmarks/bench_modes.py create mode 100644 benchmarks/bench_quality_timing.py create mode 100644 benchmarks/run_balibase_comparison.py diff --git a/.containerignore b/.containerignore index b476867..0931023 100644 --- a/.containerignore +++ b/.containerignore @@ -1,8 +1,6 @@ .git build -build-asan -build-debug -build-release +build-* benchmarks/data benchmarks/results __pycache__ diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index bd5b8e9..11a2be8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -59,10 +59,12 @@ jobs: CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*" CIBW_ARCHS: ${{ matrix.cibw_archs }} - # Set minimum macOS version to match OpenMP requirements + # Disable OpenMP on macOS to avoid libomp.dylib conflicts + # Use built-in threadpool for parallelization instead + CIBW_CONFIG_SETTINGS_MACOS: > + cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON CIBW_ENVIRONMENT_MACOS: > CMAKE_BUILD_PARALLEL_LEVEL=2 - OMP_NUM_THREADS=1 MACOSX_DEPLOYMENT_TARGET=14.0 CMAKE_OSX_DEPLOYMENT_TARGET=14.0 @@ -75,7 +77,7 @@ jobs: # macOS specific settings CIBW_BEFORE_ALL_MACOS: | - brew install --formula cmake libomp || echo "Dependencies may already be installed" + brew install --formula cmake || echo "cmake already installed" CIBW_REPAIR_WHEEL_COMMAND_MACOS: > delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a7d3f0..b04f887 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,14 +61,18 @@ include(GNUInstallDirs) include(CTest) include(CheckCSourceRuns) -option(USE_OPENMP "Use OpenMP for parallelization" ON) +option(USE_OPENMP "Use OpenMP for parallelization" OFF) +option(USE_THREADPOOL "Use built-in threadpool instead of OpenMP" ON) option(ENABLE_SSE "Enable compile-time SSE4.1 support." ON) option(ENABLE_AVX "Enable compile-time AVX support." ON) option(ENABLE_AVX2 "Enable compile-time AVX2 support." ON) # Performance tuning parameters -set(KALIGN_ALN_SERIAL_THRESHOLD "250" CACHE STRING "Alignment positions threshold below which to use serial instead of parallel processing") -set(KALIGN_KMEANS_UPGMA_THRESHOLD "50" CACHE STRING "Number of sequences threshold below which to use UPGMA instead of parallel k-means") +# Threadpool parallelization thresholds (all settings in one place) +set(KALIGN_ALN_SERIAL_THRESHOLD "500" CACHE STRING "Hirschberg DP width below which to run serial") +set(KALIGN_KMEANS_UPGMA_THRESHOLD "50" CACHE STRING "Sequence count below which to use UPGMA instead of parallel k-means") +set(KALIGN_DIST_MIN_SEQS "50" CACHE STRING "Minimum sequences to parallelize distance/anchor computations") +set(KALIGN_PFOR_MIN_CHUNK "10" CACHE STRING "Minimum iterations per chunk in tp_parallel_for") # option(ENABLE_FMA "Enable compile-time FMA support." ON) # option(ENABLE_AVX512 "Enable compile-time AVX512 support." ON) @@ -96,6 +100,20 @@ if(USE_OPENMP) endif(OPENMP_FOUND OR OpenMP_FOUND) endif(USE_OPENMP) +# Built-in threadpool (alternative to OpenMP) +if(USE_THREADPOOL) + if(USE_OPENMP AND (OPENMP_FOUND OR OpenMP_FOUND)) + message(WARNING "Both USE_OPENMP and USE_THREADPOOL are ON. Threadpool takes precedence.") + endif() + find_package(Threads QUIET) + if(Threads_FOUND) + add_definitions(-DUSE_THREADPOOL) + message(STATUS "Built-in threadpool is enabled.") + else() + set(USE_THREADPOOL OFF) + message(STATUS "pthreads not found — falling back to serial execution.") + endif() +endif() if (ENABLE_SSE) # @@ -199,6 +217,8 @@ endif(HAVE_AVX2) # Add performance tuning parameters as compile definitions add_definitions(-DKALIGN_ALN_SERIAL_THRESHOLD=${KALIGN_ALN_SERIAL_THRESHOLD}) add_definitions(-DKALIGN_KMEANS_UPGMA_THRESHOLD=${KALIGN_KMEANS_UPGMA_THRESHOLD}) +add_definitions(-DKALIGN_DIST_MIN_SEQS=${KALIGN_DIST_MIN_SEQS}) +add_definitions(-DKALIGN_PFOR_MIN_CHUNK=${KALIGN_PFOR_MIN_CHUNK}) add_subdirectory(lib) add_subdirectory(src) @@ -223,8 +243,10 @@ if(BUILD_PYTHON_MODULE) # Link against the static kalign library target_link_libraries(_core PRIVATE kalign_static) - # Link OpenMP if found - if(OpenMP_CXX_FOUND) + # Link OpenMP or threadpool + if(USE_THREADPOOL) + target_link_libraries(_core PRIVATE Threads::Threads) + elseif(OpenMP_CXX_FOUND) target_link_libraries(_core PRIVATE OpenMP::OpenMP_CXX) endif() diff --git a/Containerfile b/Containerfile index 15fe4c7..478b40b 100644 --- a/Containerfile +++ b/Containerfile @@ -50,16 +50,22 @@ RUN cd /tmp && \ COPY . /kalign WORKDIR /kalign -RUN mkdir -p build && cd build && \ - cmake -DCMAKE_BUILD_TYPE=Release .. && \ +# Build kalign twice: threadpool and OpenMP +RUN mkdir cbuild-tp && cd cbuild-tp && \ + cmake -DCMAKE_BUILD_TYPE=Release -DUSE_OPENMP=OFF -DUSE_THREADPOOL=ON .. && \ + make -j"$(nproc)" + +RUN mkdir cbuild-omp && cd cbuild-omp && \ + cmake -DCMAKE_BUILD_TYPE=Release -DUSE_OPENMP=ON -DUSE_THREADPOOL=OFF .. && \ make -j"$(nproc)" # ---------- Python environment ---------- RUN python3 -m venv /venv -ENV PATH="/venv/bin:/kalign/build/src:$PATH" +ENV PATH="/venv/bin:/kalign/cbuild-tp/src:$PATH" RUN pip install --no-cache-dir uv && \ - uv pip install --no-cache -e ".[benchmark]" + uv pip install --no-cache -e ".[benchmark]" \ + --config-settings='cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON' # ---------- Verify tools ---------- RUN which kalign && which clustalo && which mafft && which muscle @@ -72,7 +78,8 @@ COPY zig-out/kalign-linux-aarch64 /usr/local/bin/kalign RUN chmod +x /usr/local/bin/kalign # Rebuild Python module with latest source (uses cached venv layer) -RUN uv pip install --no-cache -e ".[benchmark]" +RUN uv pip install --no-cache -e ".[benchmark]" \ + --config-settings='cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON' EXPOSE 8050 diff --git a/README.md b/README.md index db40270..7bf7fd5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ alignment approach with multi-threading support. ### From source -Prerequisites: C compiler (GCC or Clang), CMake 3.18+, optionally OpenMP. +Prerequisites: C compiler (GCC or Clang), CMake 3.18+. ```bash mkdir build && cd build @@ -23,7 +23,11 @@ make test make install ``` -On macOS, `brew install libomp` for OpenMP support. +Kalign uses a built-in thread pool for parallelization (requires pthreads, available on all POSIX systems). If pthreads is not available, it falls back to serial execution. To use OpenMP instead: + +```bash +cmake -DUSE_OPENMP=ON -DUSE_THREADPOOL=OFF .. +``` ### Zig build (alternative) @@ -47,47 +51,43 @@ See [README-python.md](README-python.md) for the full Python documentation. kalign -i -o ``` -Kalign v3.5 has three modes: +Kalign has four mode presets, optimized for protein and nucleotide sequences: | Mode | Flag | Description | |------|------|-------------| -| default | (none) | Best general-purpose. | -| fast | `--fast` | Fastest. Same as kalign v3.4. | -| precise | `--precise` | Highest accuracy, ~10x slower. | +| fast | `--mode fast` | Single run, fastest. | +| default | `--mode default` | Single run with consistency anchors (default). | +| recall | `--mode recall` | Ensemble, optimized for recall. | +| accurate | `--mode accurate` | Ensemble, highest precision. | ### Examples ```bash -# Align sequences +# Align sequences (default mode) kalign -i sequences.fa -o aligned.fa # Fast mode -kalign --fast -i sequences.fa -o aligned.fa +kalign --mode fast -i sequences.fa -o aligned.fa -# Precise mode (ensemble + realign) -kalign --precise -i sequences.fa -o aligned.fa +# Accurate mode (ensemble) +kalign --mode accurate -i sequences.fa -o aligned.fa # Read from stdin cat input.fa | kalign -i - -o aligned.fa # Combine multiple input files kalign seqsA.fa seqsB.fa -o combined.fa - -# Save ensemble consensus for re-thresholding -kalign --precise -i seqs.fa -o out.fa --save-poar consensus.poar -kalign -i seqs.fa -o out2.fa --load-poar consensus.poar --min-support 3 ``` ### Options ``` +--mode Mode preset: fast, default, recall, accurate. [default] --format Output format: fasta, msf, clu. [fasta] --type Sequence type: protein, dna, rna, divergent. [auto] ---gpo Gap open penalty. [auto] ---gpe Gap extension penalty. [auto] ---tgpe Terminal gap extension penalty. [auto] ---ensemble N Run N ensemble alignments. [off] ---refine Refinement: none, all, confident. [none] +--gpo Gap open penalty (overrides preset). [auto] +--gpe Gap extension penalty (overrides preset). [auto] +--tgpe Terminal gap extension penalty (overrides preset). [auto] -n Number of threads. [auto] ``` diff --git a/benchmarks/bench_modes.py b/benchmarks/bench_modes.py deleted file mode 100644 index b6a38f3..0000000 --- a/benchmarks/bench_modes.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Benchmark kalign mode presets (fast/default/accurate) vs external tools on BAliBASE.""" - -import statistics -import time -from pathlib import Path - -from .datasets import get_cases -from .scoring import run_case, EXTERNAL_TOOLS - - -def main(): - cases = get_cases("balibase") - if not cases: - print("No BAliBASE cases found. Run: uv run python -m benchmarks --download-only") - return - - print(f"BAliBASE: {len(cases)} cases\n") - - methods = [ - ("kalign fast", dict(method="python_api", mode="fast")), - ("kalign default", dict(method="python_api", mode="default")), - ("kalign accurate", dict(method="python_api", mode="accurate")), - ("clustalo", dict(method="clustalo")), - ("mafft", dict(method="mafft")), - ("muscle", dict(method="muscle")), - ] - - all_results = {} - for label, kwargs in methods: - print(f"Running {label}...", flush=True) - results = [] - t0 = time.perf_counter() - for i, case in enumerate(cases): - r = run_case(case, n_threads=1, **kwargs) - results.append(r) - if r.error: - print(f" [{i+1}/{len(cases)}] {r.family}: ERROR {r.error}") - elapsed = time.perf_counter() - t0 - - ok = [r for r in results if not r.error] - all_results[label] = ok - if ok: - print(f" {len(ok)} cases, total {elapsed:.1f}s") - print() - - # Summary table - print(f"{'Method':<20} {'N':>4} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time(s)':>10}") - print("-" * 72) - for label, _ in methods: - ok = all_results.get(label, []) - if not ok: - print(f"{label:<20} {'0':>4} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>10}") - continue - rec = statistics.mean([r.recall for r in ok]) - pre = statistics.mean([r.precision for r in ok]) - f1 = statistics.mean([r.f1 for r in ok]) - tc = statistics.mean([r.tc for r in ok]) - t = sum(r.wall_time for r in ok) - print(f"{label:<20} {len(ok):>4} {rec:>8.3f} {pre:>8.3f} {f1:>8.3f} {tc:>8.3f} {t:>10.1f}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/bench_quality_timing.py b/benchmarks/bench_quality_timing.py new file mode 100644 index 0000000..83b00ce --- /dev/null +++ b/benchmarks/bench_quality_timing.py @@ -0,0 +1,142 @@ +"""Benchmark kalign mode presets (fast/default/recall/accurate) vs external tools. + +Compares all 4 kalign NSGA-III optimized mode presets against ClustalO, +MAFFT, and MUSCLE. Reports quality metrics and wall time. +Each timing measurement is repeated N times and the median is reported. + +Usage (inside container): + python -m benchmarks.bench_modes --threads 16 + python -m benchmarks.bench_modes --threads 8 --runs 3 --output results.json +""" + +import argparse +import json +import statistics +import time +from pathlib import Path + +from .datasets import get_cases, download_dataset +from .scoring import run_case + + +KALIGN_MODES = ["fast", "default", "recall", "accurate"] + + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark kalign modes vs external tools", + prog="python -m benchmarks.bench_modes", + ) + parser.add_argument( + "--threads", type=int, default=1, + help="Threads for all tools (default: 1)", + ) + parser.add_argument( + "--runs", type=int, default=3, + help="Timing repeats per method/case; report median (default: 3)", + ) + parser.add_argument( + "--output", type=str, default="", + help="Save results to JSON file", + ) + parser.add_argument( + "--dataset", default="balibase", + help="Dataset to benchmark (default: balibase)", + ) + args = parser.parse_args() + + download_dataset(args.dataset) + cases = get_cases(args.dataset) + if not cases: + print(f"No cases found for '{args.dataset}'.") + return + + print(f"Dataset: {args.dataset} ({len(cases)} cases)") + print(f"Threads: {args.threads}, Timing repeats: {args.runs}") + print() + + # Build method list: 4 kalign modes + 3 external tools + methods = [] + for mode in KALIGN_MODES: + methods.append((f"kalign {mode}", dict(method="python_api", mode=mode))) + for tool in ["clustalo", "mafft", "muscle"]: + methods.append((tool, dict(method=tool))) + + all_results = {} + for label, kwargs in methods: + print(f"Running {label}...", flush=True) + + # First run: collect quality metrics + quality_results = [] + timing_runs = {c.family: [] for c in cases} + t0 = time.perf_counter() + + for i, case in enumerate(cases): + r = run_case(case, n_threads=args.threads, **kwargs) + quality_results.append(r) + timing_runs[case.family].append(r.wall_time) + if r.error: + print(f" [{i+1}/{len(cases)}] {r.family}: ERROR {r.error}") + + # Additional timing repeats + for rep in range(1, args.runs): + for case in cases: + r = run_case(case, n_threads=args.threads, **kwargs) + if not r.error: + timing_runs[case.family].append(r.wall_time) + + elapsed = time.perf_counter() - t0 + ok = [r for r in quality_results if not r.error] + + if ok: + # Median wall time per case, then sum + median_times = [] + for r in ok: + times = timing_runs[r.family] + median_times.append(statistics.median(times) if times else r.wall_time) + total_median_time = sum(median_times) + + rec = statistics.mean([r.recall for r in ok]) + pre = statistics.mean([r.precision for r in ok]) + f1 = statistics.mean([r.f1 for r in ok]) + tc = statistics.mean([r.tc for r in ok]) + + all_results[label] = { + "n_cases": len(ok), + "recall": round(rec, 4), + "precision": round(pre, 4), + "f1": round(f1, 4), + "tc": round(tc, 4), + "median_total_time": round(total_median_time, 2), + "elapsed": round(elapsed, 1), + } + print(f" {len(ok)} cases, median total {total_median_time:.1f}s " + f"(elapsed {elapsed:.1f}s)") + else: + all_results[label] = {"n_cases": 0, "error": "all failed"} + print(f" All cases failed") + print() + + # Summary table + print(f"{'Method':<20} {'N':>4} {'Recall':>8} {'Prec':>8} {'F1':>8} " + f"{'TC':>8} {'Time(s)':>10}") + print("-" * 72) + for label, _ in methods: + s = all_results.get(label, {}) + if s.get("n_cases", 0) == 0: + print(f"{label:<20} {'0':>4} {'n/a':>8} {'n/a':>8} {'n/a':>8} " + f"{'n/a':>8} {'n/a':>10}") + continue + print(f"{label:<20} {s['n_cases']:>4} {s['recall']:>8.3f} {s['precision']:>8.3f} " + f"{s['f1']:>8.3f} {s['tc']:>8.3f} {s['median_total_time']:>10.1f}") + + if args.output: + out = Path(args.output) + out.parent.mkdir(parents=True, exist_ok=True) + with open(out, "w") as f: + json.dump(all_results, f, indent=2) + print(f"\nResults saved to {out}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/run_balibase_comparison.py b/benchmarks/run_balibase_comparison.py new file mode 100644 index 0000000..b422bb3 --- /dev/null +++ b/benchmarks/run_balibase_comparison.py @@ -0,0 +1,77 @@ +"""Run BAliBASE quality benchmark across all 4 kalign mode presets.""" + +import json +from pathlib import Path + +import kalign +from benchmarks.datasets import get_cases, download_dataset +from benchmarks.scoring import run_case + +download_dataset("balibase") +cases = get_cases("balibase") +print(f"Loaded {len(cases)} BAliBASE cases") + +modes = ["fast", "default", "recall", "accurate"] + +all_results = {} + +for mode in modes: + print(f"\n{'=' * 60}") + print(f" mode={mode}") + print(f"{'=' * 60}") + + recalls, precs, f1s, tcs, times = [], [], [], [], [] + errors = 0 + for i, case in enumerate(cases): + r = run_case(case, method="python_api", n_threads=8, mode=mode) + if r.error: + errors += 1 + continue + recalls.append(r.recall) + precs.append(r.precision) + f1s.append(r.f1) + tcs.append(r.tc) + times.append(r.wall_time) + if (i + 1) % 50 == 0: + n = len(f1s) + print(f" [{i+1}/{len(cases)}] F1={sum(f1s)/n:.4f} so far...") + + n = len(f1s) + avg_r = sum(recalls) / n if n else 0 + avg_p = sum(precs) / n if n else 0 + avg_f = sum(f1s) / n if n else 0 + avg_t = sum(tcs) / n if n else 0 + total_t = sum(times) + + all_results[mode] = { + "mode": mode, + "n_cases": n, + "errors": errors, + "recall": round(avg_r, 4), + "precision": round(avg_p, 4), + "f1": round(avg_f, 4), + "tc": round(avg_t, 4), + "total_time": round(total_t, 1), + } + print(f" Recall={avg_r:.4f} Prec={avg_p:.4f} " + f"F1={avg_f:.4f} TC={avg_t:.4f} " + f"Time={total_t:.1f}s ({errors} errors)") + +# Summary +print() +print("=" * 72) +print(" RESULTS") +print("=" * 72) +print() +print(f" {'Mode':<12} {'N':>4} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time':>8}") +print(f" {'-'*12} {'-'*4} {'-'*8} {'-'*8} {'-'*8} {'-'*8} {'-'*8}") +for mode in modes: + s = all_results[mode] + print(f" {s['mode']:<12} {s['n_cases']:>4} {s['recall']:>8.4f} {s['precision']:>8.4f} " + f"{s['f1']:>8.4f} {s['tc']:>8.4f} {s['total_time']:>7.1f}s") + +out_file = Path("benchmarks/results/balibase_modes.json") +out_file.parent.mkdir(exist_ok=True) +with open(out_file, "w") as f: + json.dump(all_results, f, indent=2) +print(f"\nSaved to {out_file}") diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 1d9488f..a18d8dc 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -61,17 +61,26 @@ set(source_files # src/test.h ) +# Add threadpool source when enabled +if(USE_THREADPOOL) + list(APPEND source_files src/threadpool/threadpool.c) +endif() + add_library(${PROJECT_NAME}_OBJ OBJECT ${source_files}) -if(OpenMP_C_FOUND) +if(OpenMP_C_FOUND AND NOT USE_THREADPOOL) target_link_libraries(${PROJECT_NAME}_OBJ PRIVATE OpenMP::OpenMP_C) -endif(OpenMP_C_FOUND) +endif() +if(USE_THREADPOOL) + target_link_libraries(${PROJECT_NAME}_OBJ PRIVATE Threads::Threads) +endif() target_include_directories(${PROJECT_NAME}_OBJ PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src/threadpool ) target_compile_definitions(${PROJECT_NAME}_OBJ PRIVATE KALIGN_PACKAGE_VERSION=\"${KALIGN_PACKAGE_VERSION}\") @@ -127,11 +136,12 @@ target_include_directories(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} PRIVATE m) -if(OpenMP_C_FOUND) +if(OpenMP_C_FOUND AND NOT USE_THREADPOOL) target_link_libraries(${PROJECT_NAME} PRIVATE OpenMP::OpenMP_C) endif() - - +if(USE_THREADPOOL) + target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) +endif() add_library(${PROJECT_NAME}_static STATIC ${public_header_files} @@ -164,9 +174,12 @@ target_include_directories(${PROJECT_NAME}_static target_link_libraries(${PROJECT_NAME}_static PRIVATE m) -if(OpenMP_C_FOUND) +if(OpenMP_C_FOUND AND NOT USE_THREADPOOL) target_link_libraries(${PROJECT_NAME}_static PRIVATE OpenMP::OpenMP_C) endif() +if(USE_THREADPOOL) + target_link_libraries(${PROJECT_NAME}_static PRIVATE Threads::Threads) +endif() add_library(tldevel STATIC diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 7385ad2..1b3a840 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -122,6 +122,9 @@ EXTERN int kalign_consensus_from_poar(struct msa* msa, /* Memory */ EXTERN void kalign_free_msa(struct msa* msa); +/* Query MSA properties */ +EXTERN int kalign_msa_get_biotype(struct msa *msa); + /* Auxillary... */ EXTERN int reformat_settings_msa(struct msa *msa, int rename, int unalign); diff --git a/lib/src/aln_apair_dist.c b/lib/src/aln_apair_dist.c index 42c12ce..6714439 100644 --- a/lib/src/aln_apair_dist.c +++ b/lib/src/aln_apair_dist.c @@ -1,11 +1,37 @@ #include "tldevel.h" #include "msa_struct.h" +#ifdef USE_THREADPOOL +#include "threadpool.h" +#endif + #define ALN_APAIR_DIST_IMPORT #include "aln_apair_dist.h" static float pairwise_identity_dist(const char* a, const char* b, int alnlen); +#ifdef USE_THREADPOOL +struct apair_ctx { + struct msa_seq** seqs; + float** dm; + int n; + int alnlen; +}; + +static void apair_row_fn(int row_start, int row_end, void *arg) +{ + struct apair_ctx *c = (struct apair_ctx *)arg; + for (int i = row_start; i < row_end; i++) { + const char *seq_i = c->seqs[i]->seq; + for (int j = i + 1; j < c->n; j++) { + float d = pairwise_identity_dist(seq_i, c->seqs[j]->seq, c->alnlen); + c->dm[i][j] = d; + c->dm[j][i] = d; + } + } +} +#endif + int compute_aln_pairwise_dist(struct msa* msa, float*** dm_ptr) { float** dm = NULL; @@ -26,6 +52,12 @@ int compute_aln_pairwise_dist(struct msa* msa, float*** dm_ptr) dm[i][i] = 0.0f; } +#ifdef USE_THREADPOOL + if(msa->pool && n >= KALIGN_DIST_MIN_SEQS){ + struct apair_ctx ctx = { msa->sequences, dm, n, msa->alnlen }; + tp_parallel_for_chunked(msa->pool, 0, n - 1, KALIGN_PFOR_MIN_CHUNK, apair_row_fn, &ctx); + }else{ +#endif for(i = 0; i < n - 1; i++){ const char* seq_i = msa->sequences[i]->seq; for(j = i + 1; j < n; j++){ @@ -35,6 +67,9 @@ int compute_aln_pairwise_dist(struct msa* msa, float*** dm_ptr) dm[j][i] = d; } } +#ifdef USE_THREADPOOL + } +#endif *dm_ptr = dm; return OK; diff --git a/lib/src/aln_controller.c b/lib/src/aln_controller.c index 582d27a..b748c3f 100644 --- a/lib/src/aln_controller.c +++ b/lib/src/aln_controller.c @@ -2,8 +2,6 @@ #include "tldevel.h" - - #include "aln_param.h" #include "aln_struct.h" @@ -12,10 +10,20 @@ #include "aln_seqprofile.h" #include "aln_profileprofile.h" +#ifdef USE_THREADPOOL +#include "threadpool.h" + +static void wrap_seqseq_fwd(void *arg) { aln_seqseq_foward((struct aln_mem *)arg); } +static void wrap_seqseq_bwd(void *arg) { aln_seqseq_backward((struct aln_mem *)arg); } +static void wrap_profprof_fwd(void *arg) { aln_profileprofile_foward((struct aln_mem *)arg); } +static void wrap_profprof_bwd(void *arg) { aln_profileprofile_backward((struct aln_mem *)arg); } +static void wrap_seqprof_fwd(void *arg) { aln_seqprofile_foward((struct aln_mem *)arg); } +static void wrap_seqprof_bwd(void *arg) { aln_seqprofile_backward((struct aln_mem *)arg); } +#endif + #define ALN_CONTROLLER_IMPORT #include "aln_controller.h" - static int aln_continue(struct aln_mem* m,float input_states[],int old_cor[],int meet,int transition, uint8_t serial); int aln_runner(struct aln_mem* m) @@ -61,6 +69,42 @@ int aln_runner(struct aln_mem* m) m->enda_2 = old_cor[1]; /* fprintf(stderr,"Forward:%d-%d %d-%d\n",m->starta,m->enda,m->startb,m->endb); */ +#ifdef USE_THREADPOOL + if(m->run_parallel && m->pool){ + tp_group_t *g = tp_group_create(m->pool); + if(m->seq1){ + tp_group_submit(g, wrap_seqseq_fwd, m); + tp_group_submit(g, wrap_seqseq_bwd, m); + tp_group_wait(g); + aln_seqseq_meetup(m,old_cor,&meet,&transition,&score); + }else if(m->prof2){ + tp_group_submit(g, wrap_profprof_fwd, m); + tp_group_submit(g, wrap_profprof_bwd, m); + tp_group_wait(g); + aln_profileprofile_meetup(m,old_cor,&meet,&transition,&score); + }else{ + tp_group_submit(g, wrap_seqprof_fwd, m); + tp_group_submit(g, wrap_seqprof_bwd, m); + tp_group_wait(g); + aln_seqprofile_meetup(m,old_cor,&meet,&transition,&score); + } + tp_group_destroy(g); + }else{ + if(m->seq1){ + aln_seqseq_foward(m); + aln_seqseq_backward(m); + aln_seqseq_meetup(m,old_cor,&meet,&transition,&score); + }else if(m->prof2){ + aln_profileprofile_foward(m); + aln_profileprofile_backward(m); + aln_profileprofile_meetup(m,old_cor,&meet,&transition,&score); + }else{ + aln_seqprofile_foward(m); + aln_seqprofile_backward(m); + aln_seqprofile_meetup(m,old_cor,&meet,&transition,&score); + } + } +#else #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait @@ -71,7 +115,6 @@ int aln_runner(struct aln_mem* m) #pragma omp task shared(m) if(m->run_parallel) #endif aln_seqseq_foward(m); - #ifdef HAVE_OPENMP #pragma omp task shared(m) if(m->run_parallel) #endif @@ -109,6 +152,7 @@ int aln_runner(struct aln_mem* m) } #ifdef HAVE_OPENMP } +#endif #endif if(m->mode == ALN_MODE_SCORE_ONLY){ diff --git a/lib/src/aln_mem.c b/lib/src/aln_mem.c index 7c94453..65257c9 100644 --- a/lib/src/aln_mem.c +++ b/lib/src/aln_mem.c @@ -36,6 +36,9 @@ int alloc_aln_mem(struct aln_mem** mem, int x) m->flip_n_targets = 0; m->flip_n_uncertain = 0; m->run_parallel = 0; +#ifdef USE_THREADPOOL + m->pool = NULL; +#endif m->ap = NULL; m->consistency = NULL; diff --git a/lib/src/aln_param.h b/lib/src/aln_param.h index 3d522c6..dba3219 100644 --- a/lib/src/aln_param.h +++ b/lib/src/aln_param.h @@ -11,13 +11,15 @@ #endif #endif -/* #define KALIGN_DNA 0 */ -/* #define KALIGN_DNA_INTERNAL 1 */ -/* #define KALIGN_RNA 2 */ -/* #define KALIGN_PROTEIN 3 */ +#ifdef USE_THREADPOOL +typedef struct threadpool threadpool_t; +#endif struct aln_param{ int nthreads; +#ifdef USE_THREADPOOL + threadpool_t *pool; /* shared pool for all parallel work */ +#endif /* actual parameters */ float** subm; float gpo; diff --git a/lib/src/aln_run.c b/lib/src/aln_run.c index f55a218..1775fed 100644 --- a/lib/src/aln_run.c +++ b/lib/src/aln_run.c @@ -6,6 +6,9 @@ #ifdef HAVE_OPENMP #include #endif +#ifdef USE_THREADPOOL +#include "threadpool.h" +#endif #include "task.h" @@ -41,6 +44,22 @@ static void recursive_aln_inline(struct msa* msa, struct aln_tasks*t, struct aln /* static int SampleWithoutReplacement(struct rng_state* rng, int N, int n,int* samples); */ /* static int int_cmp(const void *a, const void *b); */ +#ifdef USE_THREADPOOL +struct recursive_aln_arg { + struct msa *msa; + struct aln_tasks *t; + struct aln_param *ap; + uint8_t *active; + int c; +}; + +static void recursive_aln_task(void *arg) +{ + struct recursive_aln_arg *a = (struct recursive_aln_arg *)arg; + recursive_aln(a->msa, a->t, a->ap, a->active, a->c); +} +#endif + int create_msa_tree(struct msa* msa, struct aln_param* ap,struct aln_tasks* t) { int i; @@ -63,9 +82,11 @@ int create_msa_tree(struct msa* msa, struct aln_param* ap,struct aln_tasks* t) msa->run_parallel = 0; } +#if !defined(USE_THREADPOOL) #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait +#endif #endif recursive_aln(msa, t, ap, active, t->n_tasks-1); @@ -93,6 +114,19 @@ void recursive_aln(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, ui a = local_t->a - msa->numseq; b = local_t->b - msa->numseq; +#ifdef USE_THREADPOOL + { + struct recursive_aln_arg arg_a = { msa, t, ap, active, a }; + struct recursive_aln_arg arg_b = { msa, t, ap, active, b }; + tp_group_t *g = tp_group_create(ap->pool); + if(!active[local_t->a] && local_t->a >= msa->numseq) + tp_group_submit(g, recursive_aln_task, &arg_a); + if(!active[local_t->b] && local_t->b >= msa->numseq) + tp_group_submit(g, recursive_aln_task, &arg_b); + tp_group_wait(g); + tp_group_destroy(g); + } +#else if(!active[local_t->a] && local_t->a >= msa->numseq){ #ifdef HAVE_OPENMP #pragma omp task shared(msa,t,ap,active) firstprivate(a) @@ -107,6 +141,7 @@ void recursive_aln(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, ui } #ifdef HAVE_OPENMP #pragma omp taskwait +#endif #endif struct aln_mem* ml = NULL; @@ -116,6 +151,9 @@ void recursive_aln(struct msa* msa, struct aln_tasks*t, struct aln_param* ap, ui ml->ap = ap; ml->mode = ALN_MODE_FULL; ml->run_parallel = msa->run_parallel; +#ifdef USE_THREADPOOL + ml->pool = ap->pool; +#endif do_align(msa,t,ml,c); active[local_t->a] = 0; diff --git a/lib/src/aln_struct.h b/lib/src/aln_struct.h index 5cf9dcc..f3e3ac1 100644 --- a/lib/src/aln_struct.h +++ b/lib/src/aln_struct.h @@ -3,6 +3,10 @@ #include +#ifdef USE_THREADPOOL +typedef struct threadpool threadpool_t; +#endif + #define ALN_MODE_SCORE_ONLY 2 #define ALN_MODE_FULL 1 @@ -26,6 +30,9 @@ struct aln_mem{ int* path; int* tmp_path; uint8_t run_parallel; +#ifdef USE_THREADPOOL + threadpool_t *pool; +#endif int alloc_path_len; float score; float margin_sum; /* accumulated meetup margins */ diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index d3c78dd..c8c5f2e 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -26,6 +26,10 @@ #include #endif +#ifdef USE_THREADPOOL +#include "threadpool.h" +#endif + #define ALN_WRAP_IMPORT #include "aln_wrap.h" @@ -170,9 +174,13 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, RUN(alloc_tasks(&tasks, msa->numseq)); -#ifdef HAVE_OPENMP +#ifdef USE_THREADPOOL + threadpool_t *pool = tp_create(n_threads); + msa->pool = pool; +#elif defined(HAVE_OPENMP) omp_set_num_threads(n_threads); #endif + /* Build guide tree - noisy variant if seed != 0 */ if(tree_seed != 0 && tree_noise > 0.0f){ RUN(build_tree_kmeans_noisy(msa, &tasks, tree_seed, tree_noise)); @@ -196,6 +204,10 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, gpo, gpe, tgpe)); +#ifdef USE_THREADPOOL + ap->pool = pool; +#endif + ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; @@ -256,6 +268,10 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return OK; ERROR: if(msa->consistency_table){ @@ -264,6 +280,10 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, } aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return FAIL; } @@ -299,9 +319,13 @@ int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, RUN(alloc_tasks(&tasks, msa->numseq)); -#ifdef HAVE_OPENMP +#ifdef USE_THREADPOOL + threadpool_t *pool = tp_create(n_threads); + msa->pool = pool; +#elif defined(HAVE_OPENMP) omp_set_num_threads(n_threads); #endif + RUN(build_tree_kmeans(msa, &tasks)); if(msa->biotype == ALN_BIOTYPE_PROTEIN){ @@ -317,6 +341,10 @@ int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, gpo, gpe, tgpe)); +#ifdef USE_THREADPOOL + ap->pool = pool; +#endif + ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; @@ -358,10 +386,18 @@ int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return OK; ERROR: aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return FAIL; } @@ -396,9 +432,13 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, RUN(alloc_tasks(&tasks, msa->numseq)); -#ifdef HAVE_OPENMP +#ifdef USE_THREADPOOL + threadpool_t *pool = tp_create(n_threads); + msa->pool = pool; +#elif defined(HAVE_OPENMP) omp_set_num_threads(n_threads); #endif + /* Initial guide tree from BPM anchor distances */ RUN(build_tree_kmeans(msa, &tasks)); @@ -415,6 +455,10 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, gpo, gpe, tgpe)); +#ifdef USE_THREADPOOL + ap->pool = pool; +#endif + ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; @@ -532,6 +576,10 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return OK; ERROR: if(msa->consistency_table){ @@ -540,6 +588,10 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, } aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return FAIL; } @@ -572,6 +624,13 @@ int kalign_post_realign(struct msa *msa, int n_threads, int type, gpo, gpe, tgpe)); +#ifdef USE_THREADPOOL + threadpool_t *pool = tp_create(n_threads); + msa->pool = pool; + ap->pool = pool; +#elif defined(HAVE_OPENMP) + omp_set_num_threads(n_threads); +#endif ap->adaptive_budget = adaptive_budget; if(use_seq_weights >= 0.0f){ ap->use_seq_weights = use_seq_weights; @@ -581,10 +640,6 @@ int kalign_post_realign(struct msa *msa, int n_threads, int type, ap->vsm_amax = vsm_amax; } -#ifdef HAVE_OPENMP - omp_set_num_threads(n_threads); -#endif - DECLARE_TIMER(t1); if(!msa->quiet){ LOG_MSG("Post-realign (%d iterations, vsm_amax=%.2f)", @@ -672,10 +727,18 @@ int kalign_post_realign(struct msa *msa, int n_threads, int type, aln_param_free(ap); free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return OK; ERROR: aln_param_free(ap); if(tasks) free_tasks(tasks); +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return FAIL; } diff --git a/lib/src/bisectingKmeans.c b/lib/src/bisectingKmeans.c index e200be8..0983ef3 100644 --- a/lib/src/bisectingKmeans.c +++ b/lib/src/bisectingKmeans.c @@ -2,6 +2,9 @@ #ifdef HAVE_OPENMP #include #endif +#ifdef USE_THREADPOOL +#include "threadpool.h" +#endif #ifdef HAVE_AVX2 #include @@ -58,6 +61,38 @@ static int split(const float * const * dm, int *samples, int num_anchors, int nu int seed_pick, struct kmeans_result **ret); static int split2(const float * const * dm,const int* samples, const int num_anchors,const int num_samples,const int seed_pick,struct kmeans_result** ret); +#ifdef USE_THREADPOOL +/* Wrapper args for threadpool task submission */ +struct split2_task_arg { + const float *const *dm; + const int *samples; + int num_anchors; + int num_samples; + int seed_idx; + struct kmeans_result **res; +}; + +static void split2_task_fn(void *arg) +{ + struct split2_task_arg *a = (struct split2_task_arg *)arg; + split2(a->dm, a->samples, a->num_anchors, a->num_samples, a->seed_idx, a->res); +} + +struct bisect_task_arg { + struct msa *msa; + struct node **ret_n; + const float *const *dm; + int *samples; + int num_samples; +}; + +static void bisect_task_fn(void *arg) +{ + struct bisect_task_arg *a = (struct bisect_task_arg *)arg; + bisecting_kmeans(a->msa, a->ret_n, a->dm, a->samples, a->num_samples); +} +#endif + static inline int cmp_floats(const float a, const float b); inline int cmp_floats(const float a, const float b) @@ -131,9 +166,11 @@ int build_tree_kmeans_noisy(struct msa* msa, struct aln_tasks** tasks, LOG_MSG("Building guide tree."); } +#if !defined(USE_THREADPOOL) #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait +#endif #endif bisecting_kmeans(msa, &root, (const float * const *)dm, samples, numseq); @@ -224,9 +261,11 @@ int build_tree_kmeans(struct msa* msa, struct aln_tasks** tasks) /* if(n_threads == 1){ */ /* RUN(bisecting_kmeans_serial(msa,&root, dm, samples, numseq)); */ /* }else{ */ +#if !defined(USE_THREADPOOL) #ifdef HAVE_OPENMP #pragma omp parallel #pragma omp single nowait +#endif #endif bisecting_kmeans(msa,&root, (const float * const *)dm, samples, numseq); /* } */ @@ -287,17 +326,35 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * int num_l,num_r; /* LOG_MSG("num_samples: %d", num_samples); */ - num_anchors = MACRO_MIN(32, msa->numseq); - if(num_samples < KALIGN_KMEANS_UPGMA_THRESHOLD){ - float** dm = NULL; - RUNP(dm = d_estimation(msa, samples, num_samples,1));// anchors, num_anchors,1)); - n = upgma(dm,samples, num_samples); + /* Base cases: 0 or 1 samples cannot be split further */ + if(num_samples <= 0){ + return OK; + } + if(num_samples == 1){ + n = alloc_node(); + n->id = samples[0]; *ret_n = n; - gfree(dm); MFREE(samples); return OK; - //return n; + } + + num_anchors = MACRO_MIN(32, msa->numseq); + + /* K-means needs at least 3 samples for a non-degenerate bisection. + Floor the threshold to 3 so UPGMA handles tiny inputs safely. */ + { + int threshold = KALIGN_KMEANS_UPGMA_THRESHOLD; + if(threshold < 3) threshold = 3; + if(num_samples < threshold){ + float** dm_local = NULL; + RUNP(dm_local = d_estimation(msa, samples, num_samples,1)); + n = upgma(dm_local, samples, num_samples); + *ret_n = n; + gfree(dm_local); + MFREE(samples); + return OK; + } } /* else if(num_samples < 1000){ */ @@ -321,24 +378,43 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * for(i = 0;i < tries;i += 4){ change = 0; +#ifdef USE_THREADPOOL + { + struct split2_task_arg args[4]; + for (int t = 0; t < 4; t++) { + args[t].dm = dm; + args[t].samples = samples; + args[t].num_anchors = num_anchors; + args[t].num_samples = num_samples; + args[t].seed_idx = ((i + t) * step) % num_samples; + args[t].res = &res[t]; + } + tp_group_t *g = tp_group_create(msa->pool); + for (int t = 0; t < 4; t++) + tp_group_submit(g, split2_task_fn, &args[t]); + tp_group_wait(g); + tp_group_destroy(g); + } +#else #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif - split2(dm,samples,num_anchors, num_samples, (i)*step, &res[0]); + split2(dm,samples,num_anchors, num_samples, ((i)*step) % num_samples, &res[0]); #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif - split2(dm,samples,num_anchors, num_samples, (i+ 1)*step, &res[1]); + split2(dm,samples,num_anchors, num_samples, ((i+ 1)*step) % num_samples, &res[1]); #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif - split2(dm,samples,num_anchors, num_samples, (i+ 2)*step, &res[2]); + split2(dm,samples,num_anchors, num_samples, ((i+ 2)*step) % num_samples, &res[2]); #ifdef HAVE_OPENMP #pragma omp task shared(dm,samples,num_anchors, num_samples,i,step,res) #endif - split2(dm,samples,num_anchors, num_samples, (i+ 3)*step, &res[3]); + split2(dm,samples,num_anchors, num_samples, ((i+ 3)*step) % num_samples, &res[3]); #ifdef HAVE_OPENMP #pragma omp taskwait +#endif #endif for(j = 0; j < 4;j++){ @@ -382,9 +458,17 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * MFREE(samples); n = alloc_node(); -/* #ifdef HAVE_OPENMP */ -/* #pragma omp parallel //num_threads(2) */ -/* #pragma omp single nowait */ +#ifdef USE_THREADPOOL + { + struct bisect_task_arg left_arg = { msa, &n->left, dm, sl, num_l }; + struct bisect_task_arg right_arg = { msa, &n->right, dm, sr, num_r }; + tp_group_t *g = tp_group_create(msa->pool); + tp_group_submit(g, bisect_task_fn, &left_arg); + tp_group_submit(g, bisect_task_fn, &right_arg); + tp_group_wait(g); + tp_group_destroy(g); + } +#else #ifdef HAVE_OPENMP #pragma omp task shared(msa,n,dm) #endif @@ -397,6 +481,7 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * #ifdef HAVE_OPENMP #pragma omp taskwait +#endif #endif *ret_n =n; @@ -422,7 +507,7 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * /* num_anchors = MACRO_MIN(32, msa->numseq); */ -/* if(num_samples < KALIGN_KMEANS_UPGMA_THRESHOLD){ */ +/* if(num_samples < msa->kmeans_upgma_threshold){ */ /* float** dm = NULL; */ /* RUNP(dm = d_estimation(msa, samples, num_samples,1));// anchors, num_anchors,1)); */ /* n = upgma(dm,samples, num_samples); */ diff --git a/lib/src/msa_alloc.c b/lib/src/msa_alloc.c index b86354b..02e86ae 100644 --- a/lib/src/msa_alloc.c +++ b/lib/src/msa_alloc.c @@ -29,7 +29,6 @@ int alloc_msa(struct msa** msa, int numseq) m->consistency_table = NULL; m->poar_consistency = NULL; - MMALLOC(m->sequences, sizeof(struct msa_seq*) * m->alloc_numseq); diff --git a/lib/src/msa_op.c b/lib/src/msa_op.c index 6b25289..6358b27 100644 --- a/lib/src/msa_op.c +++ b/lib/src/msa_op.c @@ -457,6 +457,9 @@ int kalign_arr_to_msa(char** input_sequences, int* len, int numseq,struct msa** msa->col_confidence = NULL; msa->seq_weights = NULL; msa->run_parallel = 0; +#ifdef USE_THREADPOOL + msa->pool = NULL; +#endif msa->consistency_table = NULL; msa->poar_consistency = NULL; msa->quiet = 1; @@ -598,3 +601,9 @@ int make_linear_sequence(struct msa_seq* seq, char* linear_seq) ///fprintf(stdout,"LINEAR:%s\n",linear_seq); return OK; } + +int kalign_msa_get_biotype(struct msa *msa) +{ + if(!msa) return ALN_BIOTYPE_UNDEF; + return msa->biotype; +} diff --git a/lib/src/msa_struct.h b/lib/src/msa_struct.h index 6dc3b20..9433555 100644 --- a/lib/src/msa_struct.h +++ b/lib/src/msa_struct.h @@ -3,6 +3,10 @@ #include +#ifdef USE_THREADPOOL +typedef struct threadpool threadpool_t; +#endif + #ifdef MSA_STRUCT_IMPORT #define EXTERN #else @@ -40,6 +44,9 @@ struct msa{ int* nsip; int* plen; uint8_t run_parallel; +#ifdef USE_THREADPOOL + threadpool_t *pool; +#endif int numseq; int num_profiles; int alloc_numseq; diff --git a/lib/src/pick_anchor.c b/lib/src/pick_anchor.c index bc5cde1..71f29c8 100644 --- a/lib/src/pick_anchor.c +++ b/lib/src/pick_anchor.c @@ -2,6 +2,47 @@ #include "msa_struct.h" #include "bpm.h" +#ifdef USE_THREADPOOL +#include "threadpool.h" + +struct anchor_dist_ctx { + struct msa_seq** seqs; + float* min_dist; + uint8_t* anchor_s; + int anchor_len; +}; + +static void anchor_init_fn(int start, int end, void *arg) +{ + struct anchor_dist_ctx *c = (struct anchor_dist_ctx *)arg; + for (int i = start; i < end; i++) { + uint8_t *si = c->seqs[i]->s; + int li = c->seqs[i]->len; + if (li > c->anchor_len) + c->min_dist[i] = (float)BPM(si, c->anchor_s, li, c->anchor_len); + else + c->min_dist[i] = (float)BPM(c->anchor_s, si, c->anchor_len, li); + } +} + +static void anchor_update_fn(int start, int end, void *arg) +{ + struct anchor_dist_ctx *c = (struct anchor_dist_ctx *)arg; + for (int i = start; i < end; i++) { + if (c->min_dist[i] < 0.0f) continue; + uint8_t *si = c->seqs[i]->s; + int li = c->seqs[i]->len; + float d; + if (li > c->anchor_len) + d = (float)BPM(si, c->anchor_s, li, c->anchor_len); + else + d = (float)BPM(c->anchor_s, si, c->anchor_len, li); + if (d < c->min_dist[i]) + c->min_dist[i] = d; + } +} +#endif + #define PICK_ANCHOR_IMPORT #include "pick_anchor.h" @@ -54,6 +95,12 @@ static int* pick_anchor_farthest_first(struct msa* msa, int K, int* n_out) { uint8_t* anchor_s = msa->sequences[anchors[0]]->s; int anchor_len = msa->sequences[anchors[0]]->len; +#ifdef USE_THREADPOOL + if(msa->pool && numseq >= KALIGN_DIST_MIN_SEQS){ + struct anchor_dist_ctx ctx = { msa->sequences, min_dist, anchor_s, anchor_len }; + tp_parallel_for_chunked(msa->pool, 0, numseq, KALIGN_PFOR_MIN_CHUNK, anchor_init_fn, &ctx); + }else{ +#endif for(i = 0; i < numseq; i++){ uint8_t* si = msa->sequences[i]->s; int li = msa->sequences[i]->len; @@ -63,6 +110,9 @@ static int* pick_anchor_farthest_first(struct msa* msa, int K, int* n_out) min_dist[i] = (float)BPM(anchor_s, si, anchor_len, li); } } +#ifdef USE_THREADPOOL + } +#endif min_dist[anchors[0]] = -1.0f; /* mark as selected */ } @@ -83,6 +133,12 @@ static int* pick_anchor_farthest_first(struct msa* msa, int K, int* n_out) /* Update min_dist with new anchor */ uint8_t* anchor_s = msa->sequences[best_idx]->s; int anchor_len = msa->sequences[best_idx]->len; +#ifdef USE_THREADPOOL + if(msa->pool && numseq >= KALIGN_DIST_MIN_SEQS){ + struct anchor_dist_ctx ctx = { msa->sequences, min_dist, anchor_s, anchor_len }; + tp_parallel_for_chunked(msa->pool, 0, numseq, KALIGN_PFOR_MIN_CHUNK, anchor_update_fn, &ctx); + }else{ +#endif for(i = 0; i < numseq; i++){ if(min_dist[i] < 0.0f) continue; /* already selected */ uint8_t* si = msa->sequences[i]->s; @@ -97,6 +153,9 @@ static int* pick_anchor_farthest_first(struct msa* msa, int K, int* n_out) min_dist[i] = d; } } +#ifdef USE_THREADPOOL + } +#endif } MFREE(min_dist); diff --git a/lib/src/sequence_distance.c b/lib/src/sequence_distance.c index eb25684..7fb4143 100644 --- a/lib/src/sequence_distance.c +++ b/lib/src/sequence_distance.c @@ -7,6 +7,10 @@ #include "msa_struct.h" +#ifdef USE_THREADPOOL +#include "threadpool.h" +#endif + #define SEQUENCE_DISTANCE_IMPORT #include "sequence_distance.h" @@ -17,6 +21,32 @@ /* #include "misc.h" */ #include "bpm.h" +#ifdef USE_THREADPOOL +struct dist_row_ctx { + struct msa_seq** seqs; + float** dm; + int* samples; + int num_samples; +}; + +static void dist_row_fn(int row_start, int row_end, void *arg) +{ + struct dist_row_ctx *c = (struct dist_row_ctx *)arg; + for (int r = row_start; r < row_end; r++) { + uint8_t *s1 = c->seqs[r]->s; + int l1 = c->seqs[r]->len; + for (int k = 0; k < c->num_samples; k++) { + uint8_t *s2 = c->seqs[c->samples[k]]->s; + int l2 = c->seqs[c->samples[k]]->len; + c->dm[r][k] = calc_distance(s1, s2, l1, l2); + int avg = (l1 + l2) / 2; + float add = MACRO_MIN(10000.0, avg) / 10000.0; + c->dm[r][k] += add; + } + } +} +#endif + #define NODESIZE 16 /* small hash implementation */ @@ -98,8 +128,17 @@ float** d_estimation(struct msa* msa, int* samples, int num_samples,int pair) } struct msa_seq** s = msa->sequences; + +#ifdef USE_THREADPOOL + if(msa->pool && numseq >= KALIGN_DIST_MIN_SEQS){ + struct dist_row_ctx ctx = { s, dm, samples, num_samples }; + tp_parallel_for_chunked(msa->pool, 0, numseq, KALIGN_PFOR_MIN_CHUNK, dist_row_fn, &ctx); + }else{ +#endif +#if !defined(USE_THREADPOOL) #ifdef HAVE_OPENMP #pragma omp parallel for shared(dm, s) private(i, j) collapse(2) schedule(static) +#endif #endif for(i = 0; i < numseq;i++){ for(j = 0;j < num_samples;j++){ @@ -112,16 +151,14 @@ float** d_estimation(struct msa* msa, int* samples, int num_samples,int pair) s2 = s[samples[j]]->s; l2 = s[samples[j]]->len; dm[i][j] = calc_distance(s1,s2,l1,l2); - int s = (l1 + l2) / 2; - float add = MACRO_MIN(10000.0, s) / 10000.0; + int sv = (l1 + l2) / 2; + float add = MACRO_MIN(10000.0, sv) / 10000.0; dm[i][j] += add; - /* fprintf(stdout,"%f ",dm[i][j]); */ - /* dm[i][j] += (float)MACRO_MIN(l1, l2) / (float)MACRO_MAX(l1, l2); */ - /* dm[i][j] = dm[i][j] / (float) MACRO_MIN(l1, l2); */ - //dm[i][j] = dist; } - /* fprintf(stdout,"\n"); */ } +#ifdef USE_THREADPOOL + } +#endif /* fprintf(stdout,"\n"); */ diff --git a/lib/src/threadpool/threadpool.c b/lib/src/threadpool/threadpool.c index f998c47..b66b55d 100644 --- a/lib/src/threadpool/threadpool.c +++ b/lib/src/threadpool/threadpool.c @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -654,8 +655,9 @@ static void pfor_worker(void *arg) #define MAX_STACK_CHUNKS 64 -void tp_parallel_for(threadpool_t *pool, int start, int end, - void (*fn)(int, int, void *), void *arg) +void tp_parallel_for_chunked(threadpool_t *pool, int start, int end, + int min_chunk_size, + void (*fn)(int, int, void *), void *arg) { if (start >= end) return; @@ -663,6 +665,13 @@ void tp_parallel_for(threadpool_t *pool, int start, int end, int nchunks = pool->nworkers + 1; if (nchunks > n) nchunks = n; + /* Limit parallelism so each chunk has at least min_chunk_size iterations */ + if (min_chunk_size > 1) { + int max_chunks = (n + min_chunk_size - 1) / min_chunk_size; + if (nchunks > max_chunks) nchunks = max_chunks; + } + if (nchunks <= 1) { fn(start, end, arg); return; } + struct pfor_chunk stack_chunks[MAX_STACK_CHUNKS]; struct pfor_chunk *chunks = stack_chunks; int heap = 0; @@ -707,3 +716,9 @@ void tp_parallel_for(threadpool_t *pool, int start, int end, if (heap) free(chunks); } + +void tp_parallel_for(threadpool_t *pool, int start, int end, + void (*fn)(int, int, void *), void *arg) +{ + tp_parallel_for_chunked(pool, start, end, 1, fn, arg); +} diff --git a/lib/src/threadpool/threadpool.h b/lib/src/threadpool/threadpool.h index d150a50..a4e3c97 100644 --- a/lib/src/threadpool/threadpool.h +++ b/lib/src/threadpool/threadpool.h @@ -132,6 +132,14 @@ void tp_group_destroy(tp_group_t *group); void tp_parallel_for(threadpool_t *pool, int start, int end, void (*fn)(int start, int end, void *arg), void *arg); +/* Like tp_parallel_for, but limits parallelism so each chunk has at least + * min_chunk_size iterations. Useful for controlling overhead when per-iteration + * work is very small. min_chunk_size <= 1 is equivalent to tp_parallel_for. */ +void tp_parallel_for_chunked(threadpool_t *pool, int start, int end, + int min_chunk_size, + void (*fn)(int start, int end, void *arg), + void *arg); + #ifdef __cplusplus } #endif diff --git a/pyproject.toml b/pyproject.toml index 1ebcedd..9634a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ skbio = ["scikit-bio>=0.6.3"] io = ["biopython>=1.85"] # For I/O helper functions analysis = ["pandas>=2.3.0", "matplotlib>=3.9.4", "seaborn>=0.13.2"] all = ["biopython>=1.85", "scikit-bio>=0.6.3", "pandas>=2.3.0", "matplotlib>=3.9.4", "seaborn>=0.13.2"] -benchmark = ["dash>=2.14", "plotly>=5.18", "pandas>=2.0", "tqdm>=4.60", "pymoo>=0.6", "rich>=13.0", "kneed>=0.8"] +benchmark = ["dash>=2.14", "plotly>=5.18", "pandas>=2.0", "tqdm>=4.60", "pymoo>=0.6", "rich>=13.0", "kneed>=0.8", "optuna>=3.0"] # Development dependencies dev = [ @@ -161,13 +161,16 @@ before-all = [ ] repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" -# macOS settings +# macOS settings — OpenMP disabled to avoid libomp.dylib conflicts with conda/numpy [tool.cibuildwheel.macos] before-all = [ "brew install cmake || echo 'cmake already installed'", ] repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" +[tool.cibuildwheel.macos.config-settings] +"cmake.args" = "-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON" + # Windows settings [tool.cibuildwheel.windows] before-all = [ diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index d45c9fc..5ab0cec 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -78,8 +78,9 @@ def __repr__(self): REFINE_INLINE = _core.REFINE_INLINE # Mode constants -MODE_DEFAULT = "default" MODE_FAST = "fast" +MODE_DEFAULT = "default" +MODE_RECALL = "recall" MODE_ACCURATE = "accurate" MODE_PRECISE = "precise" # deprecated alias for "accurate" @@ -705,8 +706,9 @@ def align_file_to_file( "REFINE_ALL", "REFINE_CONFIDENT", "REFINE_INLINE", - "MODE_DEFAULT", "MODE_FAST", + "MODE_DEFAULT", + "MODE_RECALL", "MODE_ACCURATE", "MODE_PRECISE", "__version__", diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index ef92e4c..715b172 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -531,7 +531,7 @@ py::object align_from_file_mode( return py::make_tuple(names, aligned_sequences); } -// Align file-to-file using a named mode preset (fast/default/accurate). +// Align file-to-file using a named mode preset (fast/default/recall/accurate). // The C library provides NSGA-III optimized presets per biotype. void align_file_to_file_mode( const std::string& input_file, @@ -777,7 +777,7 @@ PYBIND11_MODULE(_core, m) { py::arg("gap_extend") = -1.0f, py::arg("terminal_gap_extend") = -1.0f, py::arg("n_threads") = 1, - "Align sequences using a named mode preset (fast/default/accurate)."); + "Align sequences using a named mode preset (fast/default/recall/accurate)."); m.def("align_mode", &align_mode, py::arg("sequences"), py::arg("mode"), @@ -819,7 +819,7 @@ PYBIND11_MODULE(_core, m) { py::arg("gap_open") = -1.0f, py::arg("gap_extend") = -1.0f, py::arg("terminal_gap_extend") = -1.0f, - "Align file to file using a named mode preset (fast/default/accurate)."); + "Align file to file using a named mode preset (fast/default/recall/accurate)."); m.def("align_file_to_file_mode", &align_file_to_file_mode, py::arg("input_file"), py::arg("output_file"), @@ -861,4 +861,5 @@ PYBIND11_MODULE(_core, m) { m.attr("REFINE_ALL") = KALIGN_REFINE_ALL; m.attr("REFINE_CONFIDENT") = KALIGN_REFINE_CONFIDENT; m.attr("REFINE_INLINE") = KALIGN_REFINE_INLINE; + } \ No newline at end of file diff --git a/python-kalign/cli.py b/python-kalign/cli.py index c2fca04..010dc41 100644 --- a/python-kalign/cli.py +++ b/python-kalign/cli.py @@ -65,7 +65,7 @@ def _build_parser() -> argparse.ArgumentParser: parser.add_argument( "--mode", default="default", - choices=["fast", "default", "accurate"], + choices=["fast", "default", "recall", "accurate"], help="Alignment mode preset (default: default).", ) parser.add_argument( diff --git a/src/parameters.c b/src/parameters.c index 4242b9f..2c8bf14 100644 --- a/src/parameters.c +++ b/src/parameters.c @@ -16,7 +16,7 @@ static int get_default_thread_count(void) { int cores = 1; - + #ifdef HAVE_OPENMP cores = omp_get_num_procs(); #elif defined(_WIN32) @@ -27,11 +27,11 @@ static int get_default_thread_count(void) cores = sysconf(_SC_NPROCESSORS_ONLN); if (cores <= 0) cores = 1; #endif - + if (cores > 1) cores = cores - 1; if (cores > 16) cores = 16; if (cores < 1) cores = 1; - + return cores; } @@ -39,42 +39,27 @@ struct parameters*init_param(void) { struct parameters* param = NULL; MMALLOC(param, sizeof(struct parameters)); - param->dist_method = KALIGNDIST_BPM; - param->aln_param_file = NULL; - param->param_set = -1; param->infile = NULL; param->num_infiles = 0; param->input = NULL; param->outfile = NULL; param->format = NULL; - param->reformat = 0; - param->rename = 0; param->help_flag = 0; param->dump_internal = 0; - param->type = -1; - param->gpo = -1.0; param->gpe = -1.0; param->tgpe = -1.0; - param->matadd = 0.0F; - param->chaos = 0; param->nthreads = get_default_thread_count(); - param->clean = 0; - param->unalign = 0; - param->refine = KALIGN_REFINE_NONE; - param->adaptive_budget = 0; - param->ensemble = 0; - param->ensemble_seed = 42; param->min_support = 0; - param->save_poar = NULL; param->load_poar = NULL; - param->consistency_anchors = 5; - param->consistency_weight = 2.0f; - param->realign = 0; - param->vsm_amax = -1.0f; /* sentinel: use C defaults */ - param->mode = 0; /* 0=default, 1=fast, 2=precise */ + param->mode = NULL; param->quiet = 0; + param->out_format = 0; + param->reformat = 0; + param->rename = 0; + param->clean = 0; + param->unalign = 0; return param; ERROR: free_parameters(param); diff --git a/src/parameters.h b/src/parameters.h index 2e7843a..f4e301c 100644 --- a/src/parameters.h +++ b/src/parameters.h @@ -14,47 +14,28 @@ #endif #endif - -#define KALIGNDIST_ALN 0 -#define KALIGNDIST_BPM 1 -#define KALIGNDIST_WU 2 - struct parameters{ char **infile; char *input; char *outfile; char* format; - char* aln_param_file; int type; float gpo; float gpe; float tgpe; - float matadd; - int chaos; - int out_format; - int param_set; - int dist_method; int num_infiles; - int reformat; - int rename; /* rename sequences - to make bali_score swallow the alignments */ - int dump_internal; + int out_format; int nthreads; - int clean; - int unalign; - int refine; - int adaptive_budget; - int ensemble; - uint64_t ensemble_seed; int min_support; - char* save_poar; char* load_poar; - int consistency_anchors; - float consistency_weight; - int realign; - float vsm_amax; - int mode; /* 0=default, 1=fast, 2=precise */ + char* mode; /* "fast", "default", "recall", "accurate" (NULL = default) */ int help_flag; int quiet; + int dump_internal; + int reformat; + int rename; + int clean; + int unalign; }; EXTERN struct parameters* init_param(void); diff --git a/src/run_kalign.c b/src/run_kalign.c index f835e5c..6d0ff5e 100644 --- a/src/run_kalign.c +++ b/src/run_kalign.c @@ -1,7 +1,6 @@ #include "tldevel.h" #include "tlmisc.h" #include "kalign/kalign.h" -/* #include "version.h" */ #include "parameters.h" #include @@ -9,34 +8,16 @@ #include #include - - - -#define OPT_SET 1 #define OPT_SHOWW 5 #define OPT_GPO 6 #define OPT_GPE 7 #define OPT_TGPE 8 - -#define OPT_NTHREADS 10 - #define OPT_ALN_TYPE 13 -#define OPT_REFINE 14 -#define OPT_ADAPTIVE_BUDGET 15 -#define OPT_ENSEMBLE 16 -#define OPT_ENSEMBLE_SEED 17 -#define OPT_MIN_SUPPORT 18 -#define OPT_SAVE_POAR 19 +#define OPT_MODE 22 #define OPT_LOAD_POAR 20 -#define OPT_CONSISTENCY 21 -#define OPT_FAST 22 -#define OPT_PRECISE 23 -#define OPT_REALIGN 24 -#define OPT_VSM_AMAX 25 -#define OPT_CONSISTENCY_WEIGHT 26 +#define OPT_MIN_SUPPORT 18 -static int set_aln_type(char* in, int* type ); -static int set_refine_mode(char* in, int* refine); +static int set_aln_type(char* in, int* type); static int run_kalign(struct parameters* param); @@ -49,43 +30,27 @@ int print_kalign_help(char * argv[]) const char usage[] = " -i -o "; char* basename = NULL; - RUN(tlfilename(argv[0], &basename)); fprintf(stdout,"\nUsage: %s %s\n\n",basename ,usage); - fprintf(stdout,"Modes:\n\n"); - fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"(default)","Consistency anchors + VSM (best general-purpose)"); - fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"--fast","VSM only, no consistency (fastest)"); - fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"--precise","Ensemble(3) + VSM + realign (highest precision)"); + fprintf(stdout,"Modes (--mode ):\n\n"); + fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"fast","Single run, fastest"); + fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"default","Single run with consistency anchors (default)"); + fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"recall","Ensemble, optimized for recall"); + fprintf(stdout,"%*s%-*s: %s\n",3,"",MESSAGE_MARGIN-3,"accurate","Ensemble, highest precision"); fprintf(stdout,"\nOptions:\n\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--format","Output format." ,"[Fasta]" ); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--type","Alignment type (rna, dna, internal)." ,"[rna]" ); - fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,"","Options: protein, divergent (protein)" ,"" ); - fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,""," rna, dna, internal (nuc)." ,"" ); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--gpo","Gap open penalty." ,"[]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--gpe","Gap extension penalty." ,"[]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--tgpe","Terminal gap extension penalty." ,"[]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--refine","Refinement mode." ,"[none]"); - fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,"","Options: none, all, confident" ,"" ); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-n/--nthreads","Number of threads." ,"[auto: N-1, max 16]"); - - fprintf(stdout,"\nEnsemble options:\n\n"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--ensemble","Number of ensemble runs." ,"[off; 5 if no value given]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--ensemble-seed","RNG seed for ensemble." ,"[42]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--min-support","Explicit consensus threshold." ,"[auto]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--save-poar","Save POAR table to file." ,"[off]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--type","Sequence type." ,"[auto]" ); + fprintf(stdout,"%*s%-*s %s %s\n",3,"",MESSAGE_MARGIN-3,"","Options: protein, dna, rna." ,"" ); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--gpo","Gap open penalty (overrides preset)." ,"[]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--gpe","Gap extension penalty (overrides preset)." ,"[]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--tgpe","Terminal gap extension penalty (overrides preset)." ,"[]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-n/--nthreads","Number of threads." ,"[auto]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--load-poar","Load POAR table for re-threshold." ,"[off]"); - fprintf(stdout,"\nAdvanced (usually managed by modes):\n\n"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--consistency","Anchor consistency (K anchors)." ,"[5]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--consistency-weight","Consistency anchor weight." ,"[2.0]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--realign","Alignment-guided tree rebuild iters." ,"[0]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--vsm-amax","VSM amplitude (0 to disable)." ,"[auto]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--adaptive-budget","Scale refinement trials by uncertainty." ,"[off]"); - fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--version (-V/-v)","Prints version." ,"[NA]" ); fprintf(stdout,"\nExamples:\n\n"); @@ -147,7 +112,6 @@ int main(int argc, char *argv[]) struct parameters* param = NULL; char* in = NULL; char* in_type = NULL; - char* in_refine = NULL; RUNP(param = init_param()); param->num_infiles = 0; @@ -155,25 +119,14 @@ int main(int argc, char *argv[]) while (1){ static struct option long_options[] ={ {"showw", 0,0,OPT_SHOWW }, - {"set", required_argument,0,OPT_SET}, {"format", required_argument, 0, 'f'}, {"type", required_argument, 0, OPT_ALN_TYPE}, {"gpo", required_argument, 0, OPT_GPO}, {"gpe", required_argument, 0, OPT_GPE}, {"tgpe", required_argument, 0, OPT_TGPE}, - {"refine", required_argument, 0, OPT_REFINE}, - {"adaptive-budget", no_argument, 0, OPT_ADAPTIVE_BUDGET}, - {"ensemble", optional_argument, 0, OPT_ENSEMBLE}, - {"ensemble-seed", required_argument, 0, OPT_ENSEMBLE_SEED}, - {"min-support", required_argument, 0, OPT_MIN_SUPPORT}, - {"save-poar", required_argument, 0, OPT_SAVE_POAR}, + {"mode", required_argument, 0, OPT_MODE}, {"load-poar", required_argument, 0, OPT_LOAD_POAR}, - {"consistency", required_argument, 0, OPT_CONSISTENCY}, - {"consistency-weight", required_argument, 0, OPT_CONSISTENCY_WEIGHT}, - {"fast", no_argument, 0, OPT_FAST}, - {"precise", no_argument, 0, OPT_PRECISE}, - {"realign", required_argument, 0, OPT_REALIGN}, - {"vsm-amax", required_argument, 0, OPT_VSM_AMAX}, + {"min-support", required_argument, 0, OPT_MIN_SUPPORT}, {"nthreads", required_argument, 0, 'n'}, {"input", required_argument, 0, 'i'}, {"infile", required_argument, 0, 'i'}, @@ -191,7 +144,6 @@ int main(int argc, char *argv[]) c = getopt_long_only (argc, argv,"i:o:f:n:hqvV",long_options, &option_index); - /* Detect the end of the options. */ if (c == -1){ break; } @@ -202,9 +154,6 @@ int main(int argc, char *argv[]) case OPT_SHOWW: showw = 1; break; - case OPT_SET: - param->param_set = atoi(optarg); - break; case 'f': param->format = optarg; break; @@ -223,51 +172,14 @@ int main(int argc, char *argv[]) case OPT_TGPE: param->tgpe = atof(optarg); break; - case OPT_REFINE: - in_refine = optarg; - break; - case OPT_ADAPTIVE_BUDGET: - param->adaptive_budget = 1; - break; - case OPT_ENSEMBLE: - if(optarg){ - param->ensemble = atoi(optarg); - }else if(optind < argc && argv[optind][0] != '-'){ - param->ensemble = atoi(argv[optind]); - optind++; - }else{ - param->ensemble = 5; - } - break; - case OPT_ENSEMBLE_SEED: - param->ensemble_seed = (uint64_t)strtoull(optarg, NULL, 10); - break; - case OPT_MIN_SUPPORT: - param->min_support = atoi(optarg); - break; - case OPT_SAVE_POAR: - param->save_poar = optarg; + case OPT_MODE: + param->mode = optarg; break; case OPT_LOAD_POAR: param->load_poar = optarg; break; - case OPT_CONSISTENCY: - param->consistency_anchors = atoi(optarg); - break; - case OPT_CONSISTENCY_WEIGHT: - param->consistency_weight = atof(optarg); - break; - case OPT_FAST: - param->mode = 1; - break; - case OPT_PRECISE: - param->mode = 2; - break; - case OPT_REALIGN: - param->realign = atoi(optarg); - break; - case OPT_VSM_AMAX: - param->vsm_amax = atof(optarg); + case OPT_MIN_SUPPORT: + param->min_support = atoi(optarg); break; case 'h': param->help_flag = 1; @@ -324,8 +236,6 @@ int main(int argc, char *argv[]) param->num_infiles = 0; - /* Use "-" to indicate stdin (like samtools/bcftools). - * NULL in the infile array signals read_file_stdin() to use stdin. */ if(in){ param->num_infiles++; } @@ -362,43 +272,9 @@ int main(int argc, char *argv[]) RUN(check_msa_format_string(param->format)); RUN(set_aln_type(in_type, ¶m->type)); - RUN(set_refine_mode(in_refine, ¶m->refine)); - - /* Apply mode presets. Explicit params (already set above) will have - * overridden their init_param() defaults, so we only fill in mode - * values for fields that are still at their init_param() defaults. */ - if(param->mode == 1){ - /* fast: no consistency anchors (unless user explicitly set --consistency) */ - if(param->consistency_anchors == 5){ /* still at default */ - param->consistency_anchors = 0; - } - }else if(param->mode == 2){ - /* precise: ensemble + realign */ - if(param->ensemble == 0){ - param->ensemble = 3; - } - if(param->realign == 0){ - param->realign = 1; - } - } - - /* if(param->chaos){ */ - /* if(param->chaos == 1){ */ - /* ERROR_MSG("Param chaos need to be bigger than 1 (currently %d)", param->chaos); */ - /* } */ - /* if(param->chaos > 10){ */ - /* ERROR_MSG("Param chaos bigger than 10 (currently %d)",param->chaos); */ - /* } */ - /* } */ RUN(run_kalign(param)); - /* if(devtest){ */ - /* for(c = 0; c < param->num_infiles;c++){ */ - /* MFREE(param->infile[c]); */ - /* } */ - /* } */ - free_parameters(param); return EXIT_SUCCESS; ERROR: @@ -409,52 +285,44 @@ int main(int argc, char *argv[]) int run_kalign(struct parameters* param) { struct msa* msa = NULL; + struct kalign_run_config runs[KALIGN_MAX_PRESET_RUNS]; + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + int n_runs = 0; if(param->num_infiles == 1){ - RUN(kalign_read_input(param->infile[0], &msa,param->quiet)); + RUN(kalign_read_input(param->infile[0], &msa, param->quiet)); }else{ - for(int i = 0; i < param->num_infiles;i++){ - RUN(kalign_read_input(param->infile[i], &msa,param->quiet)); + for(int i = 0; i < param->num_infiles; i++){ + RUN(kalign_read_input(param->infile[i], &msa, param->quiet)); } } - - if(param->load_poar != NULL){ RUN(kalign_consensus_from_poar(msa, param->load_poar, param->min_support > 0 ? param->min_support : 2)); - }else if(param->ensemble > 0){ - /* Ensemble uses the old kalign_ensemble which handles - sentinel resolution + diversity table internally. */ - RUN(kalign_ensemble(msa, - param->nthreads, - param->type, - param->ensemble, - param->gpo, - param->gpe, - param->tgpe, - param->ensemble_seed, - param->min_support, - param->save_poar, - param->refine, 0.0f, param->vsm_amax, - param->realign, -1.0f, - param->consistency_anchors, param->consistency_weight)); }else{ - /* Single-run: use kalign_align_full */ - struct kalign_run_config run = kalign_run_config_defaults(); - run.matrix = param->type; - run.gpo = param->gpo; - run.gpe = param->gpe; - run.tgpe = param->tgpe; - run.refine = param->refine; - run.adaptive_budget = param->adaptive_budget; - run.vsm_amax = param->vsm_amax; - run.seq_weights = -1.0f; - run.realign = param->realign; - RUN(kalign_align_full(msa, &run, 1, NULL, param->nthreads)); - } + /* Use mode preset (fast/default/recall/accurate). + kalign_get_mode_preset auto-selects protein vs nucleotide + based on the detected biotype. */ + const char* mode = param->mode ? param->mode : "default"; + int ret = kalign_get_mode_preset(mode, + kalign_msa_get_biotype(msa), + runs, &n_runs, &ens); + if(ret != 0){ + ERROR_MSG("Unknown mode: '%s'. Use: fast, default, recall, accurate.", mode); + } + + /* Override preset values with explicit user parameters. + Sentinel -1.0 means "use preset default". */ + for(int k = 0; k < n_runs; k++){ + if(param->gpo >= 0.0f) runs[k].gpo = param->gpo; + if(param->gpe >= 0.0f) runs[k].gpe = param->gpe; + if(param->tgpe >= 0.0f) runs[k].tgpe = param->tgpe; + } + RUN(kalign_align_full(msa, runs, n_runs, &ens, param->nthreads)); + } RUN(kalign_write_msa(msa, param->outfile, param->format)); kalign_free_msa(msa); @@ -464,8 +332,7 @@ int run_kalign(struct parameters* param) return FAIL; } - -int set_aln_type(char* in, int* type ) +int set_aln_type(char* in, int* type) { int t = 0; if(in){ @@ -477,16 +344,8 @@ int set_aln_type(char* in, int* type ) t = KALIGN_TYPE_DNA_INTERNAL; }else if(strstr(in,"protein")){ t = KALIGN_TYPE_PROTEIN; - }else if(strstr(in,"divergent")){ - t = KALIGN_TYPE_PROTEIN_DIVERGENT; - }else if(strstr(in,"pfasum43")){ - t = KALIGN_TYPE_PROTEIN_PFASUM43; - }else if(strstr(in,"pfasum60")){ - t = KALIGN_TYPE_PROTEIN_PFASUM60; - }else if(strstr(in,"pfasum")){ - t = KALIGN_TYPE_PROTEIN_PFASUM_AUTO; }else{ - ERROR_MSG("In %s not recognized.",in); + ERROR_MSG("Sequence type '%s' not recognized. Use: protein, dna, rna.",in); } }else{ t = KALIGN_TYPE_UNDEFINED; @@ -496,22 +355,3 @@ int set_aln_type(char* in, int* type ) ERROR: return FAIL; } - -int set_refine_mode(char* in, int* refine) -{ - if(in){ - if(strstr(in,"all")){ - *refine = KALIGN_REFINE_ALL; - }else if(strstr(in,"confident")){ - *refine = KALIGN_REFINE_CONFIDENT; - }else if(strstr(in,"none")){ - *refine = KALIGN_REFINE_NONE; - }else{ - ERROR_MSG("Refine mode '%s' not recognized. Use: none, all, confident.", in); - } - } - /* When in is NULL, keep the default from init_param() */ - return OK; -ERROR: - return FAIL; -} diff --git a/tests/python/test_ecosystem_integration.py b/tests/python/test_ecosystem_integration.py index 11570f2..179a1bb 100644 --- a/tests/python/test_ecosystem_integration.py +++ b/tests/python/test_ecosystem_integration.py @@ -315,10 +315,11 @@ def test_module_exports(self): "REFINE_ALL", "REFINE_CONFIDENT", "REFINE_INLINE", - "MODE_DEFAULT", "MODE_FAST", - "MODE_PRECISE", + "MODE_DEFAULT", + "MODE_RECALL", "MODE_ACCURATE", + "MODE_PRECISE", "PROTEIN_CORBLOSUM66", "__version__", "__author__", diff --git a/uv.lock b/uv.lock index b875f8a..d9d5859 100644 --- a/uv.lock +++ b/uv.lock @@ -57,6 +57,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "alembic" +version = "1.16.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mako", marker = "python_full_version < '3.10'" }, + { name = "sqlalchemy", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/ca/4dc52902cf3491892d464f5265a81e9dff094692c8a049a3ed6a05fe7ee8/alembic-1.16.5.tar.gz", hash = "sha256:a88bb7f6e513bd4301ecf4c7f2206fe93f9913f9b48dac3b78babde2d6fe765e", size = 1969868, upload-time = "2025-08-27T18:02:05.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl", hash = "sha256:e845dfe090c5ffa7b92593ae6687c5cb1a101e91fa53868497dbd79847f9dbe3", size = 247355, upload-time = "2025-08-27T18:02:07.37Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "mako", marker = "python_full_version >= '3.10'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + [[package]] name = "alive-progress" version = "3.3.0" @@ -695,6 +740,18 @@ 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 = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + [[package]] name = "contourpy" version = "1.3.0" @@ -1506,6 +1563,123 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/18/36503ea63e1ecd0a95590d7b6b8b7d227a1e4541a154e1612a231def1bdc/graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542", size = 22670, upload-time = "2025-01-15T09:48:57.241Z" }, ] +[[package]] +name = "greenlet" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/f5/3e9eafb4030588337b2a2ae4df46212956854e9069c07b53aa3caabafd47/greenlet-3.2.5.tar.gz", hash = "sha256:c816554eb33e7ecf9ba4defcb1fd8c994e59be6b4110da15480b3e7447ea4286", size = 191501, upload-time = "2026-02-20T20:08:51.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/d6/b3db928fc329b1b19ba32ffe143d2305f3aaafc583f5e1074c74ec445189/greenlet-3.2.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:34cc7cf8ab6f4b85298b01e13e881265ee7b3c1daf6bc10a2944abc15d4f87c3", size = 275803, upload-time = "2026-02-20T20:06:42.541Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ff/ab0ad4ff3d9e1faa266de4f6c79763b33fccd9265995f2940192494cc0ec/greenlet-3.2.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c11fe0cfb0ce33132f0b5d27eeadd1954976a82e5e9b60909ec2c4b884a55382", size = 633556, upload-time = "2026-02-20T20:30:41.594Z" }, + { url = "https://files.pythonhosted.org/packages/da/dd/7b3ac77099a1671af8077ecedb12c9a1be1310e4c35bb69fd34c18ab6093/greenlet-3.2.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a145f4b1c4ed7a2c94561b7f18b4beec3d3fb6f0580db22f7ed1d544e0620b34", size = 644943, upload-time = "2026-02-20T20:37:23.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/84630e9ff1dfc8b7690957c0f77834a84eabdbd9c4977c3a2d0cbd5325c2/greenlet-3.2.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1d01bdd67db3e5711e6246e451d7a0f75fae7bbf40adde129296a7f9aa7cc9", size = 639841, upload-time = "2026-02-20T20:07:17.473Z" }, + { url = "https://files.pythonhosted.org/packages/12/c4/6a2ee6c676dea7a05a3c3c1291fbc8ea44f26456b0accc891471293825af/greenlet-3.2.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd593db7ee1fa8a513a48a404f8cc4126998a48025e3f5cbbc68d51be0a6bf66", size = 588813, upload-time = "2026-02-20T20:07:56.171Z" }, + { url = "https://files.pythonhosted.org/packages/01/c0/75e75c2c993aa850292561ec80f5c263e3924e5843aa95a38716df69304c/greenlet-3.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ac8db07bced2c39b987bba13a3195f8157b0cfbce54488f86919321444a1cc3c", size = 1117377, upload-time = "2026-02-20T20:32:48.452Z" }, + { url = "https://files.pythonhosted.org/packages/ee/03/e38ebf9024a0873fe8f60f5b7bc36bfb3be5e13efe4d798240f2d1f0fb73/greenlet-3.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4544ab2cfd5912e42458b13516429e029f87d8bbcdc8d5506db772941ae12493", size = 1141246, upload-time = "2026-02-20T20:06:23.576Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7b/c6e1192c795c0c12871e199237909a6bd35757d92c8472c7c019959b8637/greenlet-3.2.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acabf468466d18017e2ae5fbf1a5a88b86b48983e550e1ae1437b69a83d9f4ac", size = 276916, upload-time = "2026-02-20T20:06:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b6/9887b559f3e1952d23052ec352e9977e808a2246c7cb8282a38337221e88/greenlet-3.2.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:472841de62d60f2cafd60edd4fd4dd7253eb70e6eaf14b8990dcaf177f4af957", size = 636107, upload-time = "2026-02-20T20:30:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/e3e48b63bbc27d660fa1d98aecb64906b90a12e686a436169c1330ef34b2/greenlet-3.2.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d951e7d628a6e8b68af469f0fe4f100ef64c4054abeb9cdafbfaa30a920c950", size = 648240, upload-time = "2026-02-20T20:37:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ac/e731ed62576e91e533b36d0d97325adc2786674ab9e48ed8a6a24f4ef4e9/greenlet-3.2.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8317d732e2ae0935d9ed2af2ea876fa714cf6f3b887a31ca150b54329b0a6e9", size = 643313, upload-time = "2026-02-20T20:07:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/64/99e5cdceb494bd4c1341c45b93f322601d2c8a5e1e4d1c7a2d24c5ed0570/greenlet-3.2.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce8aed6fdd5e07d3cbb988cbdc188266a4eb9e1a52db9ef5c6526e59962d3933", size = 591295, upload-time = "2026-02-20T20:07:57.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e9/968e11f388c2b8792d3b8b40a57984c894a3b4745dae3662dce722653bc5/greenlet-3.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:60c06b502d56d5451f60ca665691da29f79ed95e247bcf8ce5024d7bbe64acb9", size = 1120277, upload-time = "2026-02-20T20:32:50.103Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2c/b5f2c4c68d753dce08218dc5a6b21d82238fdfdc44309032f6fe24d285e6/greenlet-3.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d2a78e6f1bf3f1672df91e212a2f8314e1e7c922f065d14cbad4bc815059467", size = 1145746, upload-time = "2026-02-20T20:06:26.296Z" }, + { url = "https://files.pythonhosted.org/packages/ad/32/022b21523eee713e7550162d5ca6aed23f913cc2c6232b154b9fd9badc07/greenlet-3.2.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2acb30e77042f747ca81f0a10cc153296567e92e666c5e1b117f4595afd43352", size = 278412, upload-time = "2026-02-20T20:03:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/90/c5/8a3b0ed3cc34d8b988a44349437dfa0941f9c23ac108175f7b4ccea97111/greenlet-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:393c03c26c865f17f31d8db2f09603fadbe0581ad85a5d5908b131549fc38217", size = 644616, upload-time = "2026-02-20T20:30:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/2627bea183554695016af6cae93d7474fa90f61e5a6601a84ae7841cb720/greenlet-3.2.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04e6a202cde56043fd355fefd1552c4caa5c087528121871d950eb4f1b51fa99", size = 658813, upload-time = "2026-02-20T20:37:26.255Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1b/75a5aeff487a26ba427a3837da6372f1fe6f2a9c6b2898e28ac99d491c11/greenlet-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:45fcea7b697b91290b36eafc12fff479aca6ba6500d98ef6f34d5634c7119cbe", size = 655426, upload-time = "2026-02-20T20:07:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/53/91/9b5dfb4f3c88f8247c7a8f4c3759f0740bfa6bb0c59a9f6bf938e913df56/greenlet-3.2.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96e2bb8a56b7e1aed1dbfbbe0050cb2ecca99c7c91892fd1771e3afab63b3e3", size = 611138, upload-time = "2026-02-20T20:07:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8d/d0b086410512d9859c84e9242a9b341de9f5566011ddf3a3f6886b842b61/greenlet-3.2.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d7456e67b0be653dfe643bb37d9566cd30939c80f858e2ce6d2d54951f75b14a", size = 1126896, upload-time = "2026-02-20T20:32:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/ef/37/59fe12fe456e84ced6ba71781e28cde52a3124d1dd2077bc1727021f49fd/greenlet-3.2.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5ceb29d1f74c7280befbbfa27b9bf91ba4a07a1a00b2179a5d953fc219b16c42", size = 1154779, upload-time = "2026-02-20T20:06:27.583Z" }, + { url = "https://files.pythonhosted.org/packages/dd/95/d5d332fb73affaf7a1fbe80e49c2c7eae4f17c645af24a3b3fa25736d6f0/greenlet-3.2.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f2cc88b50b9006b324c1b9f5f3552f9d4564c78af57cdfb4c7baf4f0aa089146", size = 277166, upload-time = "2026-02-20T20:03:57.077Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/89458e20db5a4f1c64f9a0191561227e76d809941ca2d7529006d17d3450/greenlet-3.2.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e66872daffa360b2537170b73ad530f14fa31785b1bc78080125d92edf0a6def", size = 644674, upload-time = "2026-02-20T20:30:46.118Z" }, + { url = "https://files.pythonhosted.org/packages/90/f8/9962175d2f2eaa629a7fd7545abacc8c4deda3baa4e52c1526d2eb5f5546/greenlet-3.2.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c5445ddb7b586d870dad32ca9fc47c287d6022a528d194efdb8912093c5303ad", size = 658834, upload-time = "2026-02-20T20:37:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d7/826d0e080f0a7ad5ec47c8d143bbd3ca0887657bb806595fe2434d12938a/greenlet-3.2.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:752c896a8c976548faafe8a306d446c6a4c68d4fd24699b84d4393bd9ac69a8e", size = 655760, upload-time = "2026-02-20T20:07:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/33bd4c2f816be8c8e16f71740c4130adf3a66a3dd2ba29de72b9d8dd1096/greenlet-3.2.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499b809e7738c8af0ff9ac9d5dd821cb93f4293065a9237543217f0b252f950a", size = 614132, upload-time = "2026-02-20T20:08:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/48/79/f3891dcfc59097474a53cc3c624f2f2465e431ab493bda043b8c873fb20a/greenlet-3.2.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2c7429f6e9cea7cbf2637d86d3db12806ba970f7f972fcab39d6b54b4457cbaf", size = 1125286, upload-time = "2026-02-20T20:32:54.032Z" }, + { url = "https://files.pythonhosted.org/packages/ca/47/212b47e6d2d7a04c4083db1af2fdd291bc8fe99b7e3571bfa560b65fc361/greenlet-3.2.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e4b25e855800fba17713020c5c33e0a4b7a1829027719344f0c7c8870092a2", size = 1152825, upload-time = "2026-02-20T20:06:29Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/4e9b941be05f8da7ba804c6413761d2c11cca05994cbf0a015bd729419f0/greenlet-3.2.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7123b29e6bad2f3f89681be4ef316480fca798ebe8d22fbaced9cc3775007a4f", size = 277627, upload-time = "2026-02-20T20:06:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/23/cb/a73625c9a35138330014ecf3740c0d62e0c2b5e7279bb7f2586b1b199fac/greenlet-3.2.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e8fe0c72603201a86b2e038daf9b6c8570715f8779566419cff543b6ace88de", size = 690001, upload-time = "2026-02-20T20:30:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/83/49/6d1531109507bce7dfb23acf57a87013627ed3ac058851176e443a6a9134/greenlet-3.2.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:050703a60603db0e817364d69e048c70af299040c13a7e67792b9e62d4571196", size = 702953, upload-time = "2026-02-20T20:37:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/f958ee90fab93529b30cc1e4a59b27c1112b640570043a84af84da3b3b98/greenlet-3.2.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6712bfd520530eb67331813f7112d3ee18e206f48b3d026d8a96cd2d2ad20251", size = 698995, upload-time = "2026-02-20T20:07:22.663Z" }, + { url = "https://files.pythonhosted.org/packages/51/c1/a603906e79716d61f08afedaf8aed62017661457aef233d62d6e57ecd511/greenlet-3.2.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc06a78fa3ffbe2a75f1ebc7e040eacf6fa1050a9432953ab111fbbbf0d03c1", size = 661175, upload-time = "2026-02-20T20:08:01.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8f/f880ff4587d236b4d06893fb34da6b299aa0d00f6c8259673f80e1b6d63c/greenlet-3.2.5-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:dbe0e81e24982bb45907ca20152b31c2e3300ca352fdc4acbd4956e4a2cbc195", size = 274946, upload-time = "2026-02-20T20:05:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/3c/50/f6c78b8420187fdfe97fcf2e6d1dd243a7742d272c32fd4d4b1095474b37/greenlet-3.2.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15871afc0d78ec87d15d8412b337f287fc69f8f669346e391585824970931c48", size = 631781, upload-time = "2026-02-20T20:30:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/26/d6/3277f92e1961e6e9f41d9f173ea74b5c1f7065072637669f761626f26cc0/greenlet-3.2.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5bf0d7d62e356ef2e87e55e46a4e930ac165f9372760fb983b5631bb479e9d3a", size = 643740, upload-time = "2026-02-20T20:37:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6a/4f79d2e7b5ef3723fc5ffea0d6cb22627e5f95e0f19c973fa12bf1cf7891/greenlet-3.2.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dff6433742073e5b6ad40953a78a0e8cddcb3f6869e5ea635d29a810ca5e7d0", size = 638382, upload-time = "2026-02-20T20:07:23.883Z" }, + { url = "https://files.pythonhosted.org/packages/4d/59/7aadf33f23c65dbf4db27e7f5b60c414797a61e954352ae4a86c5c8b0553/greenlet-3.2.5-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdd67619cefe1cc9fcab57c8853d2bb36eca9f166c0058cc0d428d471f7c785c", size = 587516, upload-time = "2026-02-20T20:08:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/1d/46/b3422959f830de28a4eea447414e6bd7b980d755892f66ab52ad805da1c4/greenlet-3.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3828b309dfb1f117fe54867512a8265d8d4f00f8de6908eef9b885f4d8789062", size = 1115818, upload-time = "2026-02-20T20:32:55.786Z" }, + { url = "https://files.pythonhosted.org/packages/54/4a/3d1c9728f093415637cf3696909fa10852632e33e68238fb8ca60eb90de1/greenlet-3.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67725ae9fea62c95cf1aa230f1b8d4dc38f7cd14f6103d1df8a5a95657eb8e54", size = 1140219, upload-time = "2026-02-20T20:06:30.334Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + [[package]] name = "h5py" version = "3.14.0" @@ -1793,6 +1967,7 @@ analysis = [ benchmark = [ { name = "dash" }, { name = "kneed" }, + { name = "optuna" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "plotly" }, @@ -1874,6 +2049,7 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'" }, { name = "myst-parser", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=1.19.0" }, + { name = "optuna", marker = "extra == 'benchmark'", specifier = ">=3.0" }, { name = "pandas", marker = "extra == 'all'", specifier = ">=2.3.0" }, { name = "pandas", marker = "extra == 'analysis'", specifier = ">=2.3.0" }, { name = "pandas", marker = "extra == 'benchmark'", specifier = ">=2.0" }, @@ -2251,6 +2427,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, ] +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -3024,6 +3212,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] +[[package]] +name = "optuna" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic", version = "1.16.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "alembic", version = "1.18.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorlog" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/9b/62f120fb2ecbc4338bee70c5a3671c8e561714f3aa1a046b897ff142050e/optuna-4.8.0.tar.gz", hash = "sha256:6f7043e9f8ecb5e607af86a7eb00fb5ec2be26c3b08c201209a73d36aff37a38", size = 482603, upload-time = "2026-03-16T04:59:58.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/24/7c731839566d30dc70556d9824ef17692d896c15e3df627bce8c16f753e1/optuna-4.8.0-py3-none-any.whl", hash = "sha256:c57a7682679c36bfc9bca0da430698179e513874074b71bebedb0334964ab930", size = 419456, upload-time = "2026-03-16T04:59:56.977Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -4598,6 +4807,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.3.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/f1/69/c84f10a7fb0d6c50c0f6028cab1373ac1bc70a824d53bf857c33eddde5c4/sqlalchemy-2.0.48-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4599a95f9430ae0de82b52ff0d27304fe898c17cb5f4099f7438a51b9998ac77", size = 2160429, upload-time = "2026-03-02T15:44:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c8/2e0de4efcba76ae8cc84000bc0aedf45f7d2674a7d8cf66b884a03c3f310/sqlalchemy-2.0.48-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f27f9da0a7d22b9f981108fd4b62f8b5743423388915a563e651c20d06c1f457", size = 3236035, upload-time = "2026-03-02T16:01:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/0822c24212a2943b3df02a02c49b2b32ab67705eaa0d2f40f28f9c2e8084/sqlalchemy-2.0.48-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fcccbbc0c13c13702c471da398b8cd72ba740dca5859f148ae8e0e8e0d3e7e", size = 3235358, upload-time = "2026-03-02T16:07:58.002Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/f1c7c16d5ea0e4fbc14b473f02daedef8d77c582ef3c18b30b7307f85cff/sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a5b429eb84339f9f05e06083f119ad814e6d85e27ecbdf9c551dfdbb128eaf8a", size = 3185479, upload-time = "2026-03-02T16:01:32.781Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b8/95cb9642e608d02a0fd96bb3f7571b20a081313a178e1e661cc5dba37472/sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bcb8ebbf2e2c36cfe01a94f2438012c6a9d494cf80f129d9753bcdf33bfc35a6", size = 3207488, upload-time = "2026-03-02T16:07:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/24/cd/0dda04e28df0db4ed0b7d374f7eb7da8566db523dbac9f627cc6e0422c6d/sqlalchemy-2.0.48-cp39-cp39-win32.whl", hash = "sha256:e214d546c8ecb5fc22d6e6011746082abf13a9cf46eefb45769c7b31407c97b5", size = 2119494, upload-time = "2026-03-02T15:50:24.983Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1d/a98057e05608316cd3c2710f0b3d35e83cec6bdf00833b53a02235a1712f/sqlalchemy-2.0.48-cp39-cp39-win_amd64.whl", hash = "sha256:b8fc3454b4f3bd0a368001d0e968852dad45a873f8b4babd41bc302ec851a099", size = 2142903, upload-time = "2026-03-02T15:50:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + [[package]] name = "statsmodels" version = "0.14.6" From adb215aeb41549569a31b56edd9c38eb38b44f5a Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Tue, 24 Mar 2026 16:29:30 +0800 Subject: [PATCH 08/29] Add memcheck container, parallel optimizer, and stress tests --- Containerfile.memcheck | 80 ++++++ benchmarks/optimize_parallel.py | 444 ++++++++++++++++++++++++++++++++ tests/memcheck_stress.c | 430 +++++++++++++++++++++++++++++++ tests/memcheck_stress.py | 220 ++++++++++++++++ tests/run_memcheck.sh | 108 ++++++++ 5 files changed, 1282 insertions(+) create mode 100644 Containerfile.memcheck create mode 100644 benchmarks/optimize_parallel.py create mode 100644 tests/memcheck_stress.c create mode 100644 tests/memcheck_stress.py create mode 100644 tests/run_memcheck.sh diff --git a/Containerfile.memcheck b/Containerfile.memcheck new file mode 100644 index 0000000..78832b8 --- /dev/null +++ b/Containerfile.memcheck @@ -0,0 +1,80 @@ +# Kalign Memory Debugging Container +# +# Linux-based container with ASAN (detect_leaks), Valgrind, and glibc +# for finding memory bugs that don't manifest on macOS. +# +# Build: +# podman build -f Containerfile.memcheck -t kalign-memcheck . +# +# Run ASAN stress test: +# podman run -it kalign-memcheck /kalign/run_memcheck.sh asan +# +# Run Valgrind stress test: +# podman run -it kalign-memcheck /kalign/run_memcheck.sh valgrind +# +# Run Python stress test with ASAN: +# podman run -it kalign-memcheck /kalign/run_memcheck.sh python +# +# Interactive shell: +# podman run -it kalign-memcheck bash + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# System dependencies + debugging tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake g++ git curl \ + python3 python3-pip python3-venv python3-dev \ + valgrind \ + pkg-config \ + libomp-dev \ + && rm -rf /var/lib/apt/lists/* + +# ---------- Copy kalign source ---------- +COPY . /kalign +WORKDIR /kalign + +# ---------- Build 1: ASAN build (for C tests + Python module) ---------- +RUN mkdir -p build-asan && cd build-asan && \ + cmake -DCMAKE_BUILD_TYPE=ASAN -DBUILD_PYTHON_MODULE=OFF .. && \ + make -j"$(nproc)" + +# ---------- Build 2: Debug build (for Valgrind — no ASAN) ---------- +RUN mkdir -p build-debug && cd build-debug && \ + cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_PYTHON_MODULE=OFF .. && \ + make -j"$(nproc)" + +# ---------- Build 3: Release build (for Python module) ---------- +RUN mkdir -p build-release && cd build-release && \ + cmake -DCMAKE_BUILD_TYPE=Release .. && \ + make -j"$(nproc)" + +# ---------- Python environment ---------- +RUN python3 -m venv /venv +ENV PATH="/venv/bin:$PATH" + +RUN pip install --no-cache-dir uv && \ + cd /kalign && uv pip install --no-cache -e . + +# ---------- Compile C stress test (ASAN) ---------- +RUN cc -fsanitize=address -O0 -g -DDEBUG \ + -I/kalign/lib/include -I/kalign/lib/src \ + /kalign/tests/memcheck_stress.c \ + -L/kalign/build-asan/lib -lkalign_static -ltldevel \ + -fopenmp -lm \ + -o /kalign/build-asan/memcheck_stress + +# ---------- Compile C stress test (Debug, for Valgrind) ---------- +RUN cc -O0 -g -DDEBUG \ + -I/kalign/lib/include -I/kalign/lib/src \ + /kalign/tests/memcheck_stress.c \ + -L/kalign/build-debug/lib -lkalign_static -ltldevel \ + -fopenmp -lm \ + -o /kalign/build-debug/memcheck_stress + +# ---------- Entry script ---------- +COPY tests/run_memcheck.sh /kalign/run_memcheck.sh +RUN chmod +x /kalign/run_memcheck.sh + +CMD ["/kalign/run_memcheck.sh", "all"] diff --git a/benchmarks/optimize_parallel.py b/benchmarks/optimize_parallel.py new file mode 100644 index 0000000..19540e8 --- /dev/null +++ b/benchmarks/optimize_parallel.py @@ -0,0 +1,444 @@ +"""Optuna optimization of threadpool parallelization parameters. + +Runs separate optimizations at each thread count (1, 2, 4, 8, 16, 32, 64) +to find optimal thresholds AND where thread scaling stops helping. + +Usage: + uv run python benchmarks/optimize_parallel.py --n-trials 100 --fresh + uv run python benchmarks/optimize_parallel.py --n-trials 5 --dry-run +""" + +import argparse +import faulthandler +import json +import os +import signal +import sys +import time +from pathlib import Path + +import optuna + +import kalign + +# Enable faulthandler so segfaults print a traceback +faulthandler.enable() +if hasattr(signal, "SIGUSR1"): + faulthandler.register(signal.SIGUSR1) + +# --------------------------------------------------------------------------- +# Dataset generation +# --------------------------------------------------------------------------- + +DSSIM_SPECS = [ + # (name, n_seq, length, dna, n_obs) + ("prot_200", 200, 300, False, 50), + ("prot_500", 500, 300, False, 50), + ("prot_1000", 1000, 200, False, 50), + ("dna_200", 200, 500, True, 50), + ("dna_500", 500, 1000, True, 50), + ("dna_1000", 1000, 500, True, 50), + ("dna_viral", 200, 5000, True, 30), + ("dna_viral_lg", 500, 3000, True, 30), +] + +THREAD_COUNTS = [1, 2, 4, 8, 16, 32, 64] + + +def generate_dssim_datasets(cache_dir: Path) -> list[tuple[str, list[str], str]]: + """Generate DSSim datasets, caching to disk.""" + datasets = [] + cache_dir.mkdir(parents=True, exist_ok=True) + + for name, n_seq, length, dna, n_obs in DSSIM_SPECS: + cache_file = cache_dir / f"{name}.fasta" + seq_type = "dna" if dna else "protein" + + if cache_file.exists(): + seqs = [] + current: list[str] = [] + for line in cache_file.read_text().splitlines(): + if line.startswith(">"): + if current: + seqs.append("".join(current)) + current = [] + else: + current.append(line.strip()) + if current: + seqs.append("".join(current)) + print(f" {name}: {len(seqs)} seqs (cached)") + else: + print(f" {name}: generating {n_seq} seqs, len={length}, " + f"{'DNA' if dna else 'protein'}...") + seqs = kalign.generate_test_sequences( + n_seq=n_seq, n_obs=n_obs, dna=dna, length=length, seed=42 + ) + with open(cache_file, "w") as f: + for i, s in enumerate(seqs): + f.write(f">seq{i}\n{s}\n") + print(f" {name}: {len(seqs)} seqs generated") + + datasets.append((name, seqs, seq_type)) + + return datasets + + +def load_balifam100() -> list[tuple[str, list[str], str]]: + """Load BaliFam100 unaligned sequences.""" + try: + try: + from benchmarks.datasets import BALIFAM_DIR, balifam_download + except ImportError: + from datasets import BALIFAM_DIR, balifam_download + + if not BALIFAM_DIR.exists() or not any(BALIFAM_DIR.iterdir()): + print(" Downloading BaliFam100...") + balifam_download() + + in_dir = BALIFAM_DIR / "balifam100" / "in" + if not in_dir.exists(): + in_dir = BALIFAM_DIR / "in" + if not in_dir.exists(): + print(" WARNING: BaliFam100 in/ not found, skipping") + return [] + + datasets = [] + for fasta in sorted(in_dir.glob("*")): + if not fasta.is_file(): + continue + seqs = [] + current: list[str] = [] + for line in fasta.read_text().splitlines(): + if line.startswith(">"): + if current: + seqs.append("".join(current)) + current = [] + else: + current.append(line.strip()) + if current: + seqs.append("".join(current)) + if len(seqs) >= 10: + datasets.append((f"bf100_{fasta.stem}", seqs, "protein")) + + print(f" BaliFam100: {len(datasets)} families loaded") + return datasets + + except Exception as e: + print(f" WARNING: Could not load BaliFam100: {e}") + return [] + + +# --------------------------------------------------------------------------- +# Benchmark runner +# --------------------------------------------------------------------------- + + +def time_alignment(seqs: list[str], seq_type: str, mode: str) -> float: + start = time.perf_counter() + kalign.align(seqs, seq_type=seq_type, mode=mode) + return time.perf_counter() - start + + +def run_benchmark_suite( + datasets: list[tuple[str, list[str], str]], + mode: str, + trial: optuna.trial.Trial | None = None, +) -> float: + total = 0.0 + for i, (_name, seqs, seq_type) in enumerate(datasets): + t = time_alignment(seqs, seq_type, mode) + total += t + if trial is not None: + trial.report(total, i) + if trial.should_prune(): + raise optuna.TrialPruned() + return total + + +# --------------------------------------------------------------------------- +# Per-thread-count optimization +# --------------------------------------------------------------------------- + + +def optimize_for_thread_count( + nt: int, + datasets: list[tuple[str, list[str], str]], + mode: str, + n_trials: int, + db_path: Path, +) -> dict: + """Run Optuna optimization at a fixed thread count. Returns best result dict.""" + + n_repeats = 3 + + def objective(trial: optuna.trial.Trial) -> float: + aln_serial = trial.suggest_int("aln_serial_threshold", 50, 1000, step=10) + kmeans_upgma = trial.suggest_int("kmeans_upgma_threshold", 10, 150, step=5) + dist_min = trial.suggest_int("dist_min_seqs", 0, 100, step=5) + pfor_chunk = trial.suggest_int("pfor_min_chunk", 1, 32) + + sys.stderr.write( + f"[{nt}T trial {trial.number}] aln={aln_serial} km={kmeans_upgma} " + f"dist={dist_min} pfor={pfor_chunk}\n" + ) + sys.stderr.flush() + + kalign.set_parallel_config( + aln_serial_threshold=aln_serial, + kmeans_upgma_threshold=kmeans_upgma, + dist_min_seqs=dist_min, + pfor_min_chunk=pfor_chunk, + ) + kalign.set_num_threads(nt) + + try: + times = [] + for r in range(n_repeats): + t = run_benchmark_suite(datasets, mode, trial if r == 0 else None) + times.append(t) + times.sort() + return times[len(times) // 2] + except Exception: + return float("inf") + + optuna.logging.set_verbosity(optuna.logging.WARNING) + + storage = f"sqlite:///{db_path}" + study_name = f"parallel_opt_{mode}_{nt}t" + + study = optuna.create_study( + study_name=study_name, + storage=storage, + load_if_exists=True, + direction="minimize", + sampler=optuna.samplers.TPESampler(seed=42), + pruner=optuna.pruners.MedianPruner( + n_startup_trials=5, + n_warmup_steps=len(datasets) // 3, + ), + ) + + existing = len(study.trials) + remaining = max(0, n_trials - existing) + + if existing > 0 and remaining > 0: + print(f" Resuming ({existing} done, {remaining} remaining)") + + if remaining <= 0: + print(f" Already complete ({existing} trials)") + else: + t_start = time.perf_counter() + n_done = [0] + + def callback(study: optuna.study.Study, trial: optuna.trial.FrozenTrial): + n_done[0] += 1 + if trial.state == optuna.trial.TrialState.COMPLETE: + best = study.best_trial + is_new = trial.number == best.number + elapsed = time.perf_counter() - t_start + p = trial.params + bp = best.params + print( + f" [{n_done[0]:>3}/{remaining}] " + f"{trial.value:>7.1f}s " + f"aln={p['aln_serial_threshold']:<4} " + f"km={p['kmeans_upgma_threshold']:<4} " + f"dist={p['dist_min_seqs']:<4} " + f"pfor={p['pfor_min_chunk']:<3} " + f"| best={best.value:.1f}s " + f"(aln={bp['aln_serial_threshold']} km={bp['kmeans_upgma_threshold']} " + f"dist={bp['dist_min_seqs']} pfor={bp['pfor_min_chunk']}) " + f"({elapsed:.0f}s)" + f"{' ***' if is_new else ''}" + ) + elif trial.state == optuna.trial.TrialState.PRUNED: + print(f" [{n_done[0]:>3}/{remaining}] pruned") + + study.optimize(objective, n_trials=remaining, callbacks=[callback]) + + best = study.best_trial + return { + "n_threads": nt, + "best_time": best.value, + "params": best.params, + "trial_number": best.number, + } + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="Optimize threadpool parameters per thread count" + ) + parser.add_argument( + "--n-trials", type=int, default=100, + help="Optuna trials per thread count (default: 100)" + ) + parser.add_argument( + "--mode", default="fast", + choices=["fast", "default", "accurate"], + help="Kalign mode preset (default: fast)" + ) + parser.add_argument( + "--no-balifam", action="store_true", + help="Skip BaliFam100 datasets" + ) + parser.add_argument( + "--dry-run", action="store_true", + help="Just run baselines at each thread count, don't optimize" + ) + parser.add_argument( + "--cache-dir", type=str, + default=str(Path(__file__).parent / "data" / "parallel_opt"), + help="Directory for cached datasets and study DB" + ) + parser.add_argument( + "--fresh", action="store_true", + help="Delete previous studies and start fresh" + ) + parser.add_argument( + "--thread-counts", type=str, default=None, + help="Comma-separated thread counts (default: 1,2,4,8,16,32,64)" + ) + args = parser.parse_args() + + if args.thread_counts: + thread_counts = [int(x) for x in args.thread_counts.split(",")] + else: + max_cpu = os.cpu_count() or 64 + thread_counts = [t for t in THREAD_COUNTS if t <= max_cpu] + + cache_dir = Path(args.cache_dir) + db_path = cache_dir / "parallel_opt.db" + + print(f"{'='*72}") + print(f" Threadpool Parameter Optimization") + print(f" Mode: {args.mode} | Trials/thread-count: {args.n_trials}") + print(f" Thread counts: {thread_counts}") + print(f"{'='*72}") + print() + + # Generate/load datasets + print("Preparing datasets...") + datasets = generate_dssim_datasets(cache_dir) + + if not args.no_balifam: + bf = load_balifam100() + datasets.extend(bf) + + print(f"\nTotal: {len(datasets)} benchmark cases") + print() + + # Warm up + print("Warming up...") + warmup_seqs = kalign.generate_test_sequences( + n_seq=50, n_obs=20, dna=False, length=100, seed=99 + ) + kalign.align(warmup_seqs, seq_type="protein", mode=args.mode) + print() + + # Baseline at each thread count + print("Running baselines (default params at each thread count)...") + baselines = {} + kalign.set_parallel_config() + for nt in thread_counts: + kalign.set_num_threads(nt) + t = run_benchmark_suite(datasets, args.mode) + baselines[nt] = t + print(f" {nt:>2} threads: {t:.2f}s") + print() + + if args.dry_run: + print("Dry run complete.") + return + + # Fresh start + if args.fresh and db_path.exists(): + print(f"--fresh: deleting {db_path}") + db_path.unlink() + print() + + # Optimize at each thread count + results = [] + for nt in thread_counts: + print(f"--- Optimizing for {nt} threads ({args.n_trials} trials) ---") + result = optimize_for_thread_count( + nt, datasets, args.mode, args.n_trials, db_path + ) + result["baseline_time"] = baselines[nt] + results.append(result) + print() + + # Summary table + print() + print(f"{'='*90}") + print(f" SCALING & OPTIMIZATION RESULTS") + print(f"{'='*90}") + print() + print(f" {'Threads':>7} {'Baseline':>10} {'Optimized':>10} {'Speedup':>8} " + f"{'aln_ser':>8} {'km_upgma':>8} {'dist_min':>8} {'pfor_ch':>8}") + print(f" {'-'*7} {'-'*10} {'-'*10} {'-'*8} " + f"{'-'*8} {'-'*8} {'-'*8} {'-'*8}") + + best_overall = None + for r in results: + nt = r["n_threads"] + bl = r["baseline_time"] + opt = r["best_time"] + speedup = bl / opt if opt > 0 else 0 + p = r["params"] + is_best = best_overall is None or opt < best_overall["best_time"] + if is_best: + best_overall = r + marker = " <-- fastest" if is_best else "" + print( + f" {nt:>7} {bl:>9.2f}s {opt:>9.2f}s {speedup:>7.2f}x " + f"{p['aln_serial_threshold']:>8} " + f"{p['kmeans_upgma_threshold']:>8} " + f"{p['dist_min_seqs']:>8} " + f"{p['pfor_min_chunk']:>8}" + f"{marker}" + ) + + print() + print(f" Fastest overall: {best_overall['n_threads']} threads, " + f"{best_overall['best_time']:.2f}s") + print() + + # Scaling analysis + single = None + for r in results: + if r["n_threads"] == 1: + single = r["best_time"] + break + if single: + print(" Thread scaling (optimized):") + for r in results: + par_speedup = single / r["best_time"] if r["best_time"] > 0 else 0 + efficiency = par_speedup / r["n_threads"] * 100 if r["n_threads"] > 0 else 0 + bar = "#" * int(par_speedup * 2) + print(f" {r['n_threads']:>3}T: {par_speedup:>5.1f}x " + f"({efficiency:>4.0f}% eff) {bar}") + print() + + # Save results + results_dir = Path(__file__).parent / "results" + results_dir.mkdir(exist_ok=True) + out = { + "mode": args.mode, + "n_trials_per_thread_count": args.n_trials, + "n_datasets": len(datasets), + "results": results, + } + out_file = results_dir / "parallel_opt.json" + with open(out_file, "w") as f: + json.dump(out, f, indent=2) + print(f"Results saved to {out_file}") + + +if __name__ == "__main__": + main() diff --git a/tests/memcheck_stress.c b/tests/memcheck_stress.c new file mode 100644 index 0000000..2cf9ce9 --- /dev/null +++ b/tests/memcheck_stress.c @@ -0,0 +1,430 @@ +/* memcheck_stress.c — Comprehensive memory stress test for kalign. + * + * Exercises all major code paths repeatedly to surface memory bugs: + * - In-memory alignment (kalign_arr_to_msa path) + * - File-based alignment (kalign_read_input path) + * - Realignment iterations + * - Refinement (confident, inline) + * - Ensemble alignment with consensus + * - MSA comparison (simple + detailed + with mask) + * - Consistency anchors + * - VSM + seq_weights + * - Align + write + read-back + compare (full benchmark loop) + * + * Compile with ASAN: + * cc -fsanitize=address -O0 -g -DDEBUG \ + * -I../lib/include -I../lib/src \ + * memcheck_stress.c \ + * -L../build-asan/lib -lkalign_static -ltldevel \ + * -fopenmp -lm -o memcheck_stress + * + * Compile for Valgrind: + * cc -O0 -g -DDEBUG \ + * -I../lib/include -I../lib/src \ + * memcheck_stress.c \ + * -L../build-debug/lib -lkalign_static -ltldevel \ + * -fopenmp -lm -o memcheck_stress + */ + +#include +#include +#include +#include +#include + +#include "msa_struct.h" +#include "msa_alloc.h" +#include "msa_op.h" +#include "msa_cmp.h" +#include "msa_io.h" + +static int n_passed = 0; +static int n_failed = 0; + +#define RUN_TEST(fn, ...) do { \ + fprintf(stderr, "--- %s ---\n", #fn); \ + if (fn(__VA_ARGS__) == 0) { n_passed++; fprintf(stderr, " PASSED\n"); } \ + else { n_failed++; fprintf(stderr, " *** FAILED ***\n"); } \ +} while(0) + +/* ======== Test helpers ======== */ + +static char* test_protein_seqs[] = { + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQQIAATGIQIRGIVKWFNRRKEMISAYDLLAK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQQIAATGIQIRGIVKWFNRRKEMISAYDLLAK", + "MKQAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQQIAATGIQIRGIVKWFNRRKEMISAYDLLAK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPAAQFEVVHSLAKWKRQQIAATGIQIRGIVKWFNRRKEMISAYDLLAK", + "MKTVYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQQIAATGIQIRGIVKWFNRRKEMISAYDLLAK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQQIAATGIQIRGIV", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQD", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVKALPDAQFEVVHSLAKWKRQQIAATGIQIRGIVKWFNRRKEMISAYDLLAKMKTAY", +}; +static int n_protein_seqs = 8; + +static int get_lens(char** seqs, int n, int* lens) +{ + for (int i = 0; i < n; i++) + lens[i] = (int)strlen(seqs[i]); + return 0; +} + +/* ======== Tests ======== */ + +/* Test 1: Repeated in-memory alignment */ +static int test_inmem_align(int n) +{ + int lens[8]; + get_lens(test_protein_seqs, n_protein_seqs, lens); + + for (int i = 0; i < n; i++) { + char** aligned = NULL; + int aln_len = 0; + int ret = kalign(test_protein_seqs, lens, n_protein_seqs, 1, + KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, + &aligned, &aln_len); + if (ret != 0) { fprintf(stderr, " failed iter %d\n", i); return 1; } + for (int j = 0; j < n_protein_seqs; j++) free(aligned[j]); + free(aligned); + } + return 0; +} + +/* Test 2: Repeated file-based alignment */ +static int test_file_align(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) { fprintf(stderr, " read failed iter %d\n", i); return 1; } + msa->quiet = 1; + ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, KALIGN_REFINE_NONE, 0); + if (ret != 0) { fprintf(stderr, " align failed iter %d\n", i); kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 3: Repeated alignment with realign */ +static int test_realign(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_run_realign(msa, 1, KALIGN_TYPE_UNDEFINED, + -1.0f, -1.0f, -1.0f, + KALIGN_REFINE_NONE, 0, + 0.0f, -1.0f, 1, -1.0f, 0, 2.0f); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 4: Repeated alignment with refinement */ +static int test_refine(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, + KALIGN_REFINE_CONFIDENT, 0); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 5: Repeated MSA comparison (simple) */ +static int test_compare(const char* ref_file, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* ref = NULL; + struct msa* test = NULL; + float score = 0.0f; + int ret = kalign_read_input((char*)ref_file, &ref, 1); + if (ret != 0 || !ref) return 1; + ret = kalign_read_input((char*)ref_file, &test, 1); + if (ret != 0 || !test) { kalign_free_msa(ref); return 1; } + ret = kalign_msa_compare(ref, test, &score); + kalign_free_msa(ref); + kalign_free_msa(test); + if (ret != 0) return 1; + } + return 0; +} + +/* Test 6: Repeated detailed MSA comparison */ +static int test_compare_detailed(const char* ref_file, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* ref = NULL; + struct msa* test = NULL; + struct poar_score score; + int ret = kalign_read_input((char*)ref_file, &ref, 1); + if (ret != 0 || !ref) return 1; + ret = kalign_read_input((char*)ref_file, &test, 1); + if (ret != 0 || !test) { kalign_free_msa(ref); return 1; } + ret = kalign_msa_compare_detailed(ref, test, 0.2f, &score); + kalign_free_msa(ref); + kalign_free_msa(test); + if (ret != 0) return 1; + } + return 0; +} + +/* Test 7: Repeated ensemble alignment */ +static int test_ensemble(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_ensemble(msa, 1, KALIGN_TYPE_UNDEFINED, + 3, -1.0f, -1.0f, -1.0f, + 42, 0, NULL, + KALIGN_REFINE_NONE, 0.0f, -1.0f, + 0, -1.0f, 0, 2.0f); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 8: Full benchmark loop: align + write + read-back + compare */ +static int test_benchmark_loop(const char* input, const char* ref_file, int n) +{ + char tmpfile[] = "/tmp/kalign_memcheck_output.fa"; + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, + KALIGN_REFINE_NONE, 0); + if (ret != 0) { kalign_free_msa(msa); return 1; } + ret = kalign_write_msa(msa, tmpfile, "fasta"); + kalign_free_msa(msa); + if (ret != 0) return 1; + + /* Compare */ + struct msa* ref = NULL; + struct msa* test = NULL; + float sp = 0.0f; + struct poar_score detailed; + ret = kalign_read_input((char*)ref_file, &ref, 1); + if (ret != 0 || !ref) return 1; + ret = kalign_read_input(tmpfile, &test, 1); + if (ret != 0 || !test) { kalign_free_msa(ref); return 1; } + ret = kalign_msa_compare(ref, test, &sp); + kalign_free_msa(ref); ref = NULL; + kalign_free_msa(test); test = NULL; + if (ret != 0) return 1; + + /* Detailed compare */ + ret = kalign_read_input((char*)ref_file, &ref, 1); + if (ret != 0 || !ref) return 1; + ret = kalign_read_input(tmpfile, &test, 1); + if (ret != 0 || !test) { kalign_free_msa(ref); return 1; } + ret = kalign_msa_compare_detailed(ref, test, 0.2f, &detailed); + kalign_free_msa(ref); ref = NULL; + kalign_free_msa(test); test = NULL; + if (ret != 0) return 1; + } + remove(tmpfile); + return 0; +} + +/* Test 9: Consistency anchors */ +static int test_consistency(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_run_seeded(msa, 1, KALIGN_TYPE_UNDEFINED, + -1.0f, -1.0f, -1.0f, + KALIGN_REFINE_NONE, 0, + 0, 0.0f, 0.0f, -1.0f, -1.0f, + 3, 2.0f); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 10: Ensemble + realign */ +static int test_ensemble_realign(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_ensemble(msa, 1, KALIGN_TYPE_UNDEFINED, + 3, -1.0f, -1.0f, -1.0f, + 42, 0, NULL, + KALIGN_REFINE_CONFIDENT, 0.0f, -1.0f, + 1, -1.0f, 0, 2.0f); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 11: Ensemble + VSM + seq_weights */ +static int test_ensemble_vsm_sw(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_ensemble(msa, 1, KALIGN_TYPE_UNDEFINED, + 3, -1.0f, -1.0f, -1.0f, + 42, 0, NULL, + KALIGN_REFINE_CONFIDENT, 0.0f, 2.0f, + 1, 1.0f, 0, 2.0f); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 12: Inline refinement */ +static int test_inline_refine(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, + KALIGN_REFINE_INLINE, 0); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 13: Mixed parameter variations (like optimizer does) */ +static int test_param_sweep(const char* input, int n) +{ + float vsm_vals[] = {0.0f, 1.0f, 2.0f, 3.0f}; + float sw_vals[] = {0.0f, 1.0f}; + int cons_vals[] = {0, 3, 5}; + int idx = 0; + + for (int i = 0; i < n; i++) { + float vsm = vsm_vals[idx % 4]; + float sw = sw_vals[(idx / 4) % 2]; + int cons = cons_vals[(idx / 8) % 3]; + idx++; + + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + ret = kalign_run_seeded(msa, 1, KALIGN_TYPE_UNDEFINED, + -1.0f, -1.0f, -1.0f, + KALIGN_REFINE_NONE, 0, + 0, 0.0f, 0.0f, vsm, sw, + cons, 2.0f); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 14: kalign_align_full with single run configs */ +static int test_align_full_single(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.vsm_amax = 2.0f; + cfg.consistency_anchors = 3; + cfg.realign = 1; + cfg.refine = KALIGN_REFINE_CONFIDENT; + + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +/* Test 15: kalign_align_full with multi-run configs (ensemble) */ +static int test_align_full_multi(const char* input, int n) +{ + for (int i = 0; i < n; i++) { + struct msa* msa = NULL; + int ret = kalign_read_input((char*)input, &msa, 1); + if (ret != 0 || !msa) return 1; + msa->quiet = 1; + + struct kalign_run_config runs[3]; + for (int k = 0; k < 3; k++) { + runs[k] = kalign_run_config_defaults(); + runs[k].vsm_amax = 2.0f; + runs[k].realign = 1; + runs[k].refine = KALIGN_REFINE_CONFIDENT; + } + runs[1].gpo = 3.0f; runs[1].gpe = 1.5f; runs[1].tgpe = 0.5f; + runs[1].tree_seed = 43; runs[1].tree_noise = 0.2f; + runs[2].gpo = 8.0f; runs[2].gpe = 3.0f; runs[2].tgpe = 1.5f; + runs[2].tree_seed = 44; runs[2].tree_noise = 0.3f; + + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + + ret = kalign_align_full(msa, runs, 3, &ens, 1); + if (ret != 0) { kalign_free_msa(msa); return 1; } + kalign_free_msa(msa); + } + return 0; +} + +int main(int argc, char* argv[]) +{ + int n = 10; + const char* input_file = NULL; + const char* ref_file = NULL; + + if (argc < 3) { + fprintf(stderr, "Usage: %s [n_iters]\n", argv[0]); + return 1; + } + input_file = argv[1]; + ref_file = argv[2]; + if (argc > 3) n = atoi(argv[3]); + + fprintf(stderr, "=== Kalign Memory Stress Test (%d iterations per test) ===\n\n", n); + + RUN_TEST(test_inmem_align, n); + RUN_TEST(test_file_align, input_file, n); + RUN_TEST(test_realign, input_file, n); + RUN_TEST(test_refine, input_file, n); + RUN_TEST(test_compare, ref_file, n); + RUN_TEST(test_compare_detailed, ref_file, n); + RUN_TEST(test_ensemble, input_file, n); + RUN_TEST(test_benchmark_loop, input_file, ref_file, n); + RUN_TEST(test_consistency, input_file, n); + RUN_TEST(test_ensemble_realign, input_file, n); + RUN_TEST(test_ensemble_vsm_sw, input_file, n); + RUN_TEST(test_inline_refine, input_file, n); + RUN_TEST(test_param_sweep, input_file, n * 3); + RUN_TEST(test_align_full_single, input_file, n); + RUN_TEST(test_align_full_multi, input_file, n); + + fprintf(stderr, "\n=== Results: %d passed, %d failed ===\n", n_passed, n_failed); + return n_failed > 0 ? 1 : 0; +} diff --git a/tests/memcheck_stress.py b/tests/memcheck_stress.py new file mode 100644 index 0000000..406acb4 --- /dev/null +++ b/tests/memcheck_stress.py @@ -0,0 +1,220 @@ +"""Python-level memory stress test for kalign. + +Exercises all Python API paths repeatedly to surface memory bugs +in the C library or pybind11 bindings. + +Usage: + python tests/memcheck_stress.py [n_iters] +""" + +import gc +import sys +import tempfile +import traceback +from pathlib import Path + +DATA = Path(__file__).parent / "data" +UNALIGNED = DATA / "BB11001.tfa" +REFERENCE = DATA / "BB11001.msf" +UNALIGNED2 = DATA / "BB30014.tfa" +REFERENCE2 = DATA / "BB30014.msf" + +n_passed = 0 +n_failed = 0 + + +def run_test(fn, *args, **kwargs): + global n_passed, n_failed + name = fn.__name__ + print(f"--- {name} ---", flush=True) + try: + fn(*args, **kwargs) + n_passed += 1 + print(f" PASSED", flush=True) + except Exception as e: + n_failed += 1 + print(f" *** FAILED: {e} ***", flush=True) + traceback.print_exc() + + +def test_align_file_to_file(n): + """Basic align_file_to_file loop.""" + import kalign + for i in range(n): + with tempfile.NamedTemporaryFile(suffix=".fa", delete=True) as tmp: + kalign.align_file_to_file(str(UNALIGNED), tmp.name) + gc.collect() + + +def test_align_file_to_file_with_params(n): + """align_file_to_file with various parameter combinations.""" + import kalign + params_list = [ + {"vsm_amax": 0.0}, + {"vsm_amax": 2.0}, + {"vsm_amax": 2.0, "refine": "confident"}, + {"vsm_amax": 2.0, "realign": 1}, + {"consistency": 3}, + {"consistency": 5, "vsm_amax": 2.0}, + {"seq_weights": 1.0, "vsm_amax": 2.0}, + ] + for i in range(n): + params = params_list[i % len(params_list)] + with tempfile.NamedTemporaryFile(suffix=".fa", delete=True) as tmp: + kalign.align_file_to_file(str(UNALIGNED), tmp.name, **params) + gc.collect() + + +def test_align_inmem(n): + """In-memory alignment via align().""" + import kalign + seqs = [ + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQD", + "MKQAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKV", + "MKTVYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEV", + ] + for i in range(n): + result = kalign.align(seqs, mode="fast") + assert len(result) == len(seqs) + gc.collect() + + +def test_align_inmem_ensemble(n): + """In-memory alignment with ensemble.""" + import kalign + seqs = [ + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQD", + "MKQAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKV", + "MKTVYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEK", + "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEV", + ] + for i in range(n): + result = kalign.align(seqs, ensemble=3, mode="fast") + # ensemble returns (sequences, confidence) + assert isinstance(result, tuple) + gc.collect() + + +def test_align_from_file(n): + """align_from_file loop.""" + import kalign + for i in range(n): + result = kalign.align_from_file(str(UNALIGNED), mode="fast") + assert len(result.names) > 0 + gc.collect() + + +def test_align_from_file_ensemble(n): + """align_from_file with ensemble.""" + import kalign + for i in range(n): + result = kalign.align_from_file(str(UNALIGNED), ensemble=3, mode="fast") + assert result.column_confidence is not None + gc.collect() + + +def test_compare(n): + """Repeated MSA comparison.""" + import kalign + for i in range(n): + score = kalign.compare(str(REFERENCE), str(REFERENCE)) + assert score > 0 + gc.collect() + + +def test_compare_detailed(n): + """Repeated detailed MSA comparison.""" + import kalign + for i in range(n): + result = kalign.compare_detailed(str(REFERENCE), str(REFERENCE)) + assert result["recall"] > 0 + gc.collect() + + +def test_compare_detailed_all_cols(n): + """Repeated detailed comparison with max_gap_frac=-1.0.""" + import kalign + for i in range(n): + result = kalign.compare_detailed(str(REFERENCE), str(REFERENCE), + max_gap_frac=-1.0) + assert result["recall"] > 0 + gc.collect() + + +def test_full_benchmark_loop(n): + """Simulates what the benchmark does: align + compare + compare_detailed.""" + import kalign + for i in range(n): + with tempfile.NamedTemporaryFile(suffix=".fa", delete=True) as tmp: + kalign.align_file_to_file(str(UNALIGNED), tmp.name, mode="fast") + sp = kalign.compare(str(REFERENCE), tmp.name) + detailed = kalign.compare_detailed(str(REFERENCE), tmp.name) + gc.collect() + + +def test_full_benchmark_loop_precise(n): + """Full loop with ensemble+realign (the precise/expensive path).""" + import kalign + for i in range(n): + with tempfile.NamedTemporaryFile(suffix=".fa", delete=True) as tmp: + kalign.align_file_to_file(str(UNALIGNED), tmp.name, + ensemble=3, realign=1, + vsm_amax=2.0, refine="confident", + mode="fast") + sp = kalign.compare(str(REFERENCE), tmp.name) + detailed = kalign.compare_detailed(str(REFERENCE), tmp.name) + gc.collect() + + +def test_generate_sequences(n): + """Repeated sequence generation.""" + import kalign + for i in range(n): + seqs = kalign.generate_test_sequences(20, 10, False, 100, seed=i) + assert len(seqs) == 20 + gc.collect() + + +def test_multiple_datasets(n): + """Alternate between different datasets (like a real benchmark).""" + import kalign + datasets = [ + (UNALIGNED, REFERENCE), + (UNALIGNED2, REFERENCE2), + ] + for i in range(n): + unaln, ref = datasets[i % len(datasets)] + with tempfile.NamedTemporaryFile(suffix=".fa", delete=True) as tmp: + kalign.align_file_to_file(str(unaln), tmp.name, mode="fast") + sp = kalign.compare(str(ref), tmp.name) + detailed = kalign.compare_detailed(str(ref), tmp.name) + gc.collect() + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 10 + print(f"=== Python Memory Stress Test ({n} iters per test) ===\n", flush=True) + + run_test(test_align_file_to_file, n) + run_test(test_align_file_to_file_with_params, n * 2) + run_test(test_align_inmem, n) + run_test(test_align_inmem_ensemble, n) + run_test(test_align_from_file, n) + run_test(test_align_from_file_ensemble, n) + run_test(test_compare, n) + run_test(test_compare_detailed, n) + run_test(test_compare_detailed_all_cols, n) + run_test(test_full_benchmark_loop, n) + run_test(test_full_benchmark_loop_precise, n) + run_test(test_generate_sequences, n) + run_test(test_multiple_datasets, n * 2) + + print(f"\n=== Results: {n_passed} passed, {n_failed} failed ===", flush=True) + sys.exit(1 if n_failed > 0 else 0) + + +if __name__ == "__main__": + main() diff --git a/tests/run_memcheck.sh b/tests/run_memcheck.sh new file mode 100644 index 0000000..1a11437 --- /dev/null +++ b/tests/run_memcheck.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# run_memcheck.sh — Run memory checking tools on kalign +# +# Usage: +# ./run_memcheck.sh asan # ASAN + leak detection (C tests) +# ./run_memcheck.sh valgrind # Valgrind (C tests) +# ./run_memcheck.sh python # Python stress test (normal build) +# ./run_memcheck.sh all # All of the above + +set -e + +MODE="${1:-all}" +N_ITERS="${2:-10}" + +TESTDATA="/kalign/tests/data" +INPUT="$TESTDATA/BB11001.tfa" +REF="$TESTDATA/BB11001.msf" +INPUT2="$TESTDATA/BB30014.tfa" +REF2="$TESTDATA/BB30014.msf" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +banner() { + echo "" + echo -e "${YELLOW}============================================================${NC}" + echo -e "${YELLOW} $1${NC}" + echo -e "${YELLOW}============================================================${NC}" + echo "" +} + +# ---- ASAN stress test ---- +run_asan() { + banner "ASAN + Leak Detection (C stress test, $N_ITERS iters)" + + export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1" + + echo "--- Testing with BB11001 ---" + /kalign/build-asan/memcheck_stress "$INPUT" "$REF" "$N_ITERS" + + echo "" + echo "--- Testing with BB30014 ---" + /kalign/build-asan/memcheck_stress "$INPUT2" "$REF2" "$N_ITERS" + + echo "" + echo "--- ASAN built-in tests ---" + cd /kalign/build-asan && ctest --output-on-failure + + echo -e "\n${GREEN}ASAN tests complete.${NC}" +} + +# ---- Valgrind stress test ---- +run_valgrind() { + banner "Valgrind Memcheck (C stress test, $N_ITERS iters)" + + echo "--- Testing with BB11001 ---" + valgrind --leak-check=full \ + --show-leak-kinds=all \ + --track-origins=yes \ + --error-exitcode=1 \ + --errors-for-leak-kinds=definite \ + /kalign/build-debug/memcheck_stress "$INPUT" "$REF" "$N_ITERS" + + echo "" + echo "--- Testing with BB30014 ---" + valgrind --leak-check=full \ + --show-leak-kinds=all \ + --track-origins=yes \ + --error-exitcode=1 \ + --errors-for-leak-kinds=definite \ + /kalign/build-debug/memcheck_stress "$INPUT2" "$REF2" "$N_ITERS" + + echo -e "\n${GREEN}Valgrind tests complete.${NC}" +} + +# ---- Python stress test ---- +run_python() { + banner "Python Memory Stress Test ($N_ITERS iters)" + + python /kalign/tests/memcheck_stress.py "$N_ITERS" + + echo -e "\n${GREEN}Python stress tests complete.${NC}" +} + +# ---- Main ---- +case "$MODE" in + asan) + run_asan + ;; + valgrind) + run_valgrind + ;; + python) + run_python + ;; + all) + run_asan + run_valgrind + run_python + banner "ALL CHECKS COMPLETE" + ;; + *) + echo "Usage: $0 {asan|valgrind|python|all} [n_iters]" + exit 1 + ;; +esac From 8fd3dafa7e9addf38e5d55a2690f7714c8af799a Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Wed, 25 Mar 2026 20:28:31 +0800 Subject: [PATCH 09/29] Consolidate entry points, parallelize alignment pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major refactor of the C library internals for better threading: API consolidation: - Replace 7 redundant entry points (kalign_run, kalign_run_seeded, kalign_run_dist_scale, kalign_run_realign, kalign_post_realign, kalign_ensemble, kalign_ensemble_custom) with one internal kalign_single_run() + kalign_align_full() public entry point - One threadpool created per kalign_align_full() call, shared across all work (ensemble runs, tree traversal, anchor consistency, etc.) Parallelized components (all via threadpool fork-join or parallel-for): - Inline refine tree traversal (create_msa_tree_inline_refine) - Hirschberg fwd/bwd within inline refine edges - Concurrent ensemble runs (5 runs share the global pool) - Anchor consistency build (N×K pairwise DPs) - POAR extraction (per-pair, disjoint writes) - POAR scoring (per-row accumulation + sequential reduction) - Consensus candidate enumeration (two-pass count+fill) - Residue confidence computation (per-sequence) Realign tree improvement: - Replace O(N³) UPGMA on N×N distance matrix with O(N·K·log N) bisecting k-means on N×K anchor distances from aligned sequences - Add pair_dist_fn callback to bisecting_kmeans for pluggable leaf cluster distance computation (BPM for initial tree, identity for realign) - No N×N matrix allocated at any point in the realign path Quality: fast/default byte-identical to previous version. Recall F1 -0.001, accurate F1 -0.004 on BAliBASE (218 cases, XML core block scoring). Threading is deterministic across thread counts. Speedup at 8 threads (DSSim 1000 sequences): fast 2.1x, default 2.4x, recall 1.3x, accurate 1.4x Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/include/kalign/kalign.h | 52 +-- lib/src/aln_apair_dist.c | 278 +++++++++++- lib/src/aln_apair_dist.h | 24 +- lib/src/aln_run.c | 59 ++- lib/src/aln_wrap.c | 633 ++++++-------------------- lib/src/aln_wrap.h | 34 +- lib/src/anchor_consistency.c | 125 +++++- lib/src/bisectingKmeans.c | 97 +++- lib/src/bisectingKmeans.h | 14 +- lib/src/consensus_msa.c | 330 ++++++++++++-- lib/src/consensus_msa.h | 22 + lib/src/ensemble.c | 841 ++++++++++------------------------- lib/src/ensemble.h | 20 - lib/src/poar.c | 80 +++- lib/src/poar.h | 9 +- scripts/bench_accuracy.py | 125 ++++++ scripts/verify_balibase.py | 266 +++++++++++ tests/dssim_test.c | 7 +- tests/kalign_api_test.c | 173 +++---- tests/kalign_ensemble_test.c | 109 ++--- tests/kalign_lib_test.c | 7 +- tests/large_benchmark.c | 5 +- tests/memcheck_stress.c | 105 ++--- 23 files changed, 1867 insertions(+), 1548 deletions(-) create mode 100644 scripts/bench_accuracy.py create mode 100644 scripts/verify_balibase.py diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 1b3a840..86234b5 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -60,61 +60,11 @@ EXTERN int kalign_write_msa(struct msa *msa, char *outfile, char *format); /* EXTERN int kalign_arr_to_msa(char **input_sequences, int *len, int numseq, struct msa **multiple_aln); */ +/* Legacy C API — thin wrapper around kalign_align_full */ EXTERN int kalign(char **seq, int *len, int numseq, int n_threads, int type, float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len); -EXTERN int kalign_run(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget); - -EXTERN int kalign_run_seeded(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - uint64_t tree_seed, float tree_noise, - float dist_scale, float vsm_amax, - float use_seq_weights, - int consistency_anchors, float consistency_weight); - -EXTERN int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - float use_seq_weights); - -EXTERN int kalign_run_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights, - int consistency_anchors, float consistency_weight); - -EXTERN int kalign_post_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights); - -EXTERN int kalign_ensemble(struct msa* msa, int n_threads, int type, - int n_runs, float gpo, float gpe, float tgpe, - uint64_t seed, int min_support, - const char* save_poar_path, - int refine, float dist_scale, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight); - -EXTERN int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, - int n_runs, - const float* run_gpo, - const float* run_gpe, - const float* run_tgpe, - const int* run_types, - const float* run_noise, - uint64_t seed, int min_support, - int refine, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight); - EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); diff --git a/lib/src/aln_apair_dist.c b/lib/src/aln_apair_dist.c index 6714439..5d05997 100644 --- a/lib/src/aln_apair_dist.c +++ b/lib/src/aln_apair_dist.c @@ -8,7 +8,9 @@ #define ALN_APAIR_DIST_IMPORT #include "aln_apair_dist.h" -static float pairwise_identity_dist(const char* a, const char* b, int alnlen); +float pairwise_identity_dist(const char* a, const char* b, int alnlen); + +#define REALIGN_NUM_ANCHORS 32 #ifdef USE_THREADPOOL struct apair_ctx { @@ -95,8 +97,7 @@ void free_aln_dm(float** dm, int n) /* Distance = 1.0 - identity. Only counts columns where both sequences have a residue (no gap). */ -float pairwise_identity_dist(const char* a, const char* b, int alnlen) -{ +float pairwise_identity_dist(const char* a, const char* b, int alnlen) { int matches = 0; int aligned = 0; int i; @@ -115,3 +116,274 @@ float pairwise_identity_dist(const char* a, const char* b, int alnlen) } return 1.0f - (float)matches / (float)aligned; } + +/* ======================================================================== */ +/* Pairwise identity distance callback for bisecting k-means leaf clusters. */ +/* ======================================================================== */ + +float** aln_identity_pair_dist(struct msa* msa, int* samples, int n) +{ + float** dm = NULL; + int i, j; + + RUN(galloc(&dm, n, n)); + + for(i = 0; i < n; i++){ + dm[i][i] = 0.0f; + const char* seq_i = msa->sequences[samples[i]]->seq; + for(j = i + 1; j < n; j++){ + const char* seq_j = msa->sequences[samples[j]]->seq; + float d = pairwise_identity_dist(seq_i, seq_j, msa->alnlen); + /* Add small length-preference bonus matching d_estimation */ + int avg_len = (msa->sequences[samples[i]]->len + + msa->sequences[samples[j]]->len) / 2; + float add = (float)(avg_len < 10000 ? avg_len : 10000) / 10000.0f; + d += add; + dm[i][j] = d; + dm[j][i] = d; + } + } + + return dm; +ERROR: + return NULL; +} + +/* ======================================================================== */ +/* Anchor selection and N×K distances from aligned sequences. */ +/* Used by the realign loop to replace the O(N²) pairwise + O(N³) UPGMA */ +/* with O(N×K) distances + parallel bisecting k-means. */ +/* ======================================================================== */ + +#ifdef USE_THREADPOOL +struct anchor_aln_ctx { + struct msa_seq** seqs; + float* min_dist; + int anchor_idx; + int alnlen; +}; + +static void anchor_aln_init_fn(int start, int end, void* arg) +{ + struct anchor_aln_ctx* c = (struct anchor_aln_ctx*)arg; + const char* anchor_seq = c->seqs[c->anchor_idx]->seq; + for(int i = start; i < end; i++){ + c->min_dist[i] = pairwise_identity_dist( + c->seqs[i]->seq, anchor_seq, c->alnlen); + } +} + +static void anchor_aln_update_fn(int start, int end, void* arg) +{ + struct anchor_aln_ctx* c = (struct anchor_aln_ctx*)arg; + const char* anchor_seq = c->seqs[c->anchor_idx]->seq; + for(int i = start; i < end; i++){ + if(c->min_dist[i] < 0.0f) continue; + float d = pairwise_identity_dist( + c->seqs[i]->seq, anchor_seq, c->alnlen); + if(d < c->min_dist[i]){ + c->min_dist[i] = d; + } + } +} + +struct anchor_dm_ctx { + struct msa_seq** seqs; + int* anchors; + int K; + int alnlen; + float** dm; +}; + +static void anchor_dm_row_fn(int start, int end, void* arg) +{ + struct anchor_dm_ctx* c = (struct anchor_dm_ctx*)arg; + for(int i = start; i < end; i++){ + const char* seq_i = c->seqs[i]->seq; + for(int k = 0; k < c->K; k++){ + c->dm[i][k] = pairwise_identity_dist( + seq_i, c->seqs[c->anchors[k]]->seq, c->alnlen); + } + } +} +#endif + +int pick_anchor_from_alignment(struct msa* msa, int K, + int** anchors_out, int* K_out) +{ + int numseq = msa->numseq; + int* anchors = NULL; + float* min_dist = NULL; + int i, k; + + ASSERT(msa != NULL, "No MSA"); + ASSERT(msa->aligned == ALN_STATUS_FINAL, "MSA must be finalized"); + + if(K > numseq) K = numseq; + if(K < 1) K = 1; + + MMALLOC(anchors, sizeof(int) * K); + MMALLOC(min_dist, sizeof(float) * numseq); + + /* Pick first anchor: sequence closest to mean length */ + { + float mean_len = 0.0f; + float best_diff = 1e30f; + int best_idx = 0; + for(i = 0; i < numseq; i++){ + mean_len += (float)msa->sequences[i]->len; + } + mean_len /= (float)numseq; + for(i = 0; i < numseq; i++){ + float diff = (float)msa->sequences[i]->len - mean_len; + if(diff < 0) diff = -diff; + if(diff < best_diff){ + best_diff = diff; + best_idx = i; + } + } + anchors[0] = best_idx; + } + + /* Initialize min_dist: identity distance from each seq to first anchor */ +#ifdef USE_THREADPOOL + if(msa->pool && numseq >= KALIGN_DIST_MIN_SEQS){ + struct anchor_aln_ctx ctx = { msa->sequences, min_dist, + anchors[0], msa->alnlen }; + tp_parallel_for_chunked(msa->pool, 0, numseq, + KALIGN_PFOR_MIN_CHUNK, + anchor_aln_init_fn, &ctx); + }else{ +#endif + for(i = 0; i < numseq; i++){ + min_dist[i] = pairwise_identity_dist( + msa->sequences[i]->seq, + msa->sequences[anchors[0]]->seq, + msa->alnlen); + } +#ifdef USE_THREADPOOL + } +#endif + min_dist[anchors[0]] = -1.0f; + + /* Farthest-first: pick remaining K-1 anchors */ + for(k = 1; k < K; k++){ + float best_min = -1.0f; + int best_idx = 0; + for(i = 0; i < numseq; i++){ + if(min_dist[i] > best_min){ + best_min = min_dist[i]; + best_idx = i; + } + } + anchors[k] = best_idx; + min_dist[best_idx] = -1.0f; + + /* Update min_dist with new anchor */ +#ifdef USE_THREADPOOL + if(msa->pool && numseq >= KALIGN_DIST_MIN_SEQS){ + struct anchor_aln_ctx ctx = { msa->sequences, min_dist, + best_idx, msa->alnlen }; + tp_parallel_for_chunked(msa->pool, 0, numseq, + KALIGN_PFOR_MIN_CHUNK, + anchor_aln_update_fn, &ctx); + }else{ +#endif + for(i = 0; i < numseq; i++){ + if(min_dist[i] < 0.0f) continue; + float d = pairwise_identity_dist( + msa->sequences[i]->seq, + msa->sequences[best_idx]->seq, + msa->alnlen); + if(d < min_dist[i]){ + min_dist[i] = d; + } + } +#ifdef USE_THREADPOOL + } +#endif + } + + MFREE(min_dist); + *anchors_out = anchors; + *K_out = K; + return OK; +ERROR: + if(anchors) MFREE(anchors); + if(min_dist) MFREE(min_dist); + return FAIL; +} + +int compute_aln_anchor_dist(struct msa* msa, int* anchors, int K, + float*** dm_out) +{ + float** dm = NULL; + int numseq = msa->numseq; + int i, j; + int padded_K; + + ASSERT(msa != NULL, "No MSA"); + ASSERT(msa->aligned == ALN_STATUS_FINAL, "MSA must be finalized"); + ASSERT(anchors != NULL, "No anchors"); + ASSERT(K > 0, "K must be > 0"); + + /* Pad K to multiple of 8 for AVX2 alignment (matches d_estimation) */ + padded_K = K / 8; + if(K % 8) padded_K++; + padded_K <<= 3; + + MMALLOC(dm, sizeof(float*) * numseq); + for(i = 0; i < numseq; i++){ + dm[i] = NULL; + } + for(i = 0; i < numseq; i++){ +#ifdef HAVE_AVX2 + dm[i] = _mm_malloc(sizeof(float) * padded_K, 32); + if(!dm[i]) goto ERROR; +#else + MMALLOC(dm[i], sizeof(float) * padded_K); +#endif + for(j = 0; j < padded_K; j++){ + dm[i][j] = 0.0f; + } + } + + /* Fill N×K: identity distance from each seq to each anchor */ +#ifdef USE_THREADPOOL + if(msa->pool && numseq >= KALIGN_DIST_MIN_SEQS){ + struct anchor_dm_ctx ctx = { msa->sequences, anchors, K, + msa->alnlen, dm }; + tp_parallel_for_chunked(msa->pool, 0, numseq, + KALIGN_PFOR_MIN_CHUNK, + anchor_dm_row_fn, &ctx); + }else{ +#endif + for(i = 0; i < numseq; i++){ + const char* seq_i = msa->sequences[i]->seq; + for(j = 0; j < K; j++){ + dm[i][j] = pairwise_identity_dist( + seq_i, msa->sequences[anchors[j]]->seq, + msa->alnlen); + } + } +#ifdef USE_THREADPOOL + } +#endif + + *dm_out = dm; + return OK; +ERROR: + if(dm){ + for(i = 0; i < numseq; i++){ + if(dm[i]){ +#ifdef HAVE_AVX2 + _mm_free(dm[i]); +#else + MFREE(dm[i]); +#endif + } + } + MFREE(dm); + } + return FAIL; +} diff --git a/lib/src/aln_apair_dist.h b/lib/src/aln_apair_dist.h index fa73a6a..160447d 100644 --- a/lib/src/aln_apair_dist.h +++ b/lib/src/aln_apair_dist.h @@ -11,17 +11,39 @@ #endif #endif +#define REALIGN_NUM_ANCHORS 32 + struct msa; /* Compute NxN pairwise identity distances from an aligned MSA. Distance = 1.0 - (matches / aligned_positions) for each pair. The MSA must be finalized (sequences contain gap characters). - Caller must free the returned matrix with free_dm(). */ + Caller must free the returned matrix with free_aln_dm(). */ EXTERN int compute_aln_pairwise_dist(struct msa* msa, float*** dm_ptr); /* Free an NxN distance matrix allocated by compute_aln_pairwise_dist. */ EXTERN void free_aln_dm(float** dm, int n); +/* Identity distance between two aligned sequences (0=identical, 1=no matches). + Only counts columns where both have a residue (no gap). */ +EXTERN float pairwise_identity_dist(const char* a, const char* b, int alnlen); + +/* Pairwise identity distance callback for bisecting k-means leaf clusters. + Returns an n×n distance matrix (allocated with galloc, freed by gfree). + Sequences must be finalized (seq->seq has gap characters, msa->alnlen set). */ +EXTERN float** aln_identity_pair_dist(struct msa* msa, int* samples, int n); + +/* Select K diverse anchors from a finalized alignment using farthest-first + traversal with identity distance. Returns anchor indices and actual K. */ +EXTERN int pick_anchor_from_alignment(struct msa* msa, int K, + int** anchors_out, int* K_out); + +/* Compute N×K identity distances from each sequence to K anchors. + Output format matches d_estimation(pair=0): rows AVX2-padded. + The MSA must be finalized. Caller frees with free_aln_anchor_dm(). */ +EXTERN int compute_aln_anchor_dist(struct msa* msa, int* anchors, int K, + float*** dm_out); + #undef ALN_APAIR_DIST_IMPORT #undef EXTERN diff --git a/lib/src/aln_run.c b/lib/src/aln_run.c index 1775fed..cde6148 100644 --- a/lib/src/aln_run.c +++ b/lib/src/aln_run.c @@ -58,6 +58,21 @@ static void recursive_aln_task(void *arg) struct recursive_aln_arg *a = (struct recursive_aln_arg *)arg; recursive_aln(a->msa, a->t, a->ap, a->active, a->c); } + +struct recursive_aln_inline_arg { + struct msa *msa; + struct aln_tasks *t; + struct aln_param *ap; + uint8_t *active; + int c; + int n_trials; +}; + +static void recursive_aln_inline_task(void *arg) +{ + struct recursive_aln_inline_arg *a = (struct recursive_aln_inline_arg *)arg; + recursive_aln_inline(a->msa, a->t, a->ap, a->active, a->c, a->n_trials); +} #endif int create_msa_tree(struct msa* msa, struct aln_param* ap,struct aln_tasks* t) @@ -510,9 +525,20 @@ int create_msa_tree_inline_refine(struct msa* msa, struct aln_param* ap, active[i] = 0; } - /* Inline refine is sequential — multi-trial per edge isn't thread-safe */ - msa->run_parallel = 0; + /* Tree-level fork-join is safe (sibling edges are independent). + Multi-trial loop within each edge stays serial. + Hirschberg fwd/bwd parallelism enabled via run_parallel. */ + msa->run_parallel = 1; + if(ap->nthreads == 1){ + msa->run_parallel = 0; + } +#if !defined(USE_THREADPOOL) +#ifdef HAVE_OPENMP +#pragma omp parallel +#pragma omp single nowait +#endif +#endif recursive_aln_inline(msa, t, ap, active, t->n_tasks - 1, n_trials); MFREE(active); @@ -535,17 +561,46 @@ void recursive_aln_inline(struct msa* msa, struct aln_tasks* t, a = local_t->a - msa->numseq; b = local_t->b - msa->numseq; + /* Fork-join: sibling subtrees are independent — same pattern as recursive_aln */ +#ifdef USE_THREADPOOL + { + struct recursive_aln_inline_arg arg_a = { msa, t, ap, active, a, n_trials }; + struct recursive_aln_inline_arg arg_b = { msa, t, ap, active, b, n_trials }; + tp_group_t *g = tp_group_create(ap->pool); + if(!active[local_t->a] && local_t->a >= msa->numseq) + tp_group_submit(g, recursive_aln_inline_task, &arg_a); + if(!active[local_t->b] && local_t->b >= msa->numseq) + tp_group_submit(g, recursive_aln_inline_task, &arg_b); + tp_group_wait(g); + tp_group_destroy(g); + } +#else if(!active[local_t->a] && local_t->a >= msa->numseq){ +#ifdef HAVE_OPENMP +#pragma omp task shared(msa,t,ap,active) firstprivate(a,n_trials) +#endif recursive_aln_inline(msa, t, ap, active, a, n_trials); } if(!active[local_t->b] && local_t->b >= msa->numseq){ +#ifdef HAVE_OPENMP +#pragma omp task shared(msa,t,ap,active) firstprivate(b,n_trials) +#endif recursive_aln_inline(msa, t, ap, active, b, n_trials); } +#ifdef HAVE_OPENMP +#pragma omp taskwait +#endif +#endif + /* After children complete: align this edge (multi-trial stays serial) */ struct aln_mem* ml = NULL; alloc_aln_mem(&ml, 256); ml->ap = ap; ml->mode = ALN_MODE_FULL; + ml->run_parallel = msa->run_parallel; +#ifdef USE_THREADPOOL + ml->pool = ap->pool; +#endif do_align_inline_refine(msa, t, ml, c, n_trials); diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index c8c5f2e..9f99db6 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -118,50 +118,34 @@ static int compute_tree_weights(struct msa* msa, struct aln_tasks* tasks) return FAIL; } -int kalign(char **seq, int *len, int numseq,int n_threads, int type, float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len) -{ - struct msa *msa = NULL; - RUN(kalign_arr_to_msa(seq, len,numseq, &msa)); - - msa->quiet = 1; - if(n_threads < 1){ - n_threads = 1; - } - RUN(kalign_run(msa,n_threads, type, gpo, gpe, tgpe, KALIGN_REFINE_NONE, 0)); - - RUN(kalign_msa_to_arr(msa, aligned, out_aln_len)); - - kalign_free_msa(msa); - - return OK; -ERROR: - if(msa){ - kalign_free_msa(msa); - } - return FAIL; -} +/* ======================================================================== */ +/* kalign_single_run — unified internal entry point for one alignment run. */ +/* */ +/* Handles the full pipeline: input check, sort, alphabet conversion, tree */ +/* building, alignment, refinement, and optional realign iterations. */ +/* */ +/* The caller owns msa->pool (if USE_THREADPOOL). This function does NOT */ +/* create or destroy the threadpool — it reads msa->pool for parallel work. */ +/* ======================================================================== */ -int kalign_run_seeded(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - uint64_t tree_seed, float tree_noise, - float dist_scale, float vsm_amax, - float use_seq_weights, - int consistency_anchors, float consistency_weight) +int kalign_single_run(struct msa *msa, + const struct kalign_run_config *cfg, + int n_threads) { struct aln_tasks* tasks = NULL; struct aln_param* ap = NULL; - /* This also adds the ranks of the sequences ! */ + int type; + int iter; + + /* Input check (also sets ranks) */ RUN(kalign_essential_input_check(msa, 0)); - /* If already aligned unalign ! */ if(msa->aligned != ALN_STATUS_UNALIGNED){ RUN(dealign_msa(msa)); } - /* Make sure sequences are in order */ RUN(msa_sort_len_name(msa)); - /* Convert into internal representation */ + /* Convert to reduced alphabet for tree building */ if(msa->biotype == ALN_BIOTYPE_DNA){ msa->L = ALPHA_defDNA; RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); @@ -174,61 +158,49 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, RUN(alloc_tasks(&tasks, msa->numseq)); -#ifdef USE_THREADPOOL - threadpool_t *pool = tp_create(n_threads); - msa->pool = pool; -#elif defined(HAVE_OPENMP) +#if !defined(USE_THREADPOOL) && defined(HAVE_OPENMP) omp_set_num_threads(n_threads); #endif - /* Build guide tree - noisy variant if seed != 0 */ - if(tree_seed != 0 && tree_noise > 0.0f){ - RUN(build_tree_kmeans_noisy(msa, &tasks, tree_seed, tree_noise)); + /* Build guide tree — noisy variant for ensemble diversity */ + if(cfg->tree_seed != 0 && cfg->tree_noise > 0.0f){ + RUN(build_tree_kmeans_noisy(msa, &tasks, cfg->tree_seed, cfg->tree_noise)); }else{ RUN(build_tree_kmeans(msa, &tasks)); } - /* Convert to full alphabet after having converted to reduced alphabet for tree building above */ + /* Convert to full alphabet for alignment */ if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } /* Resolve auto matrix selection using BPM distances */ + type = cfg->matrix; RUN(resolve_matrix_auto(msa, &type)); - /* align */ - RUN(aln_param_init(&ap, - msa->biotype, - n_threads, - type, - gpo, - gpe, - tgpe)); + /* Init alignment parameters from config */ + RUN(aln_param_init(&ap, msa->biotype, n_threads, type, + cfg->gpo, cfg->gpe, cfg->tgpe)); #ifdef USE_THREADPOOL - ap->pool = pool; + ap->pool = msa->pool; #endif - ap->adaptive_budget = adaptive_budget; - if(use_seq_weights >= 0.0f){ - ap->use_seq_weights = use_seq_weights; - } - if(dist_scale > 0.0f){ - ap->dist_scale = dist_scale; - } - if(vsm_amax >= 0.0f){ - ap->vsm_amax = vsm_amax; - } + /* Apply config overrides (config values are concrete, not sentinels) */ + ap->adaptive_budget = cfg->adaptive_budget; + ap->use_seq_weights = cfg->seq_weights; + ap->dist_scale = cfg->dist_scale; + ap->vsm_amax = cfg->vsm_amax; if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } /* Build anchor consistency table if requested */ - if(consistency_anchors > 0){ - ap->consistency_anchors = consistency_anchors; - ap->consistency_weight = consistency_weight; - RUN(anchor_consistency_build(msa, ap, consistency_anchors, - consistency_weight, + if(cfg->consistency_anchors > 0){ + ap->consistency_anchors = cfg->consistency_anchors; + ap->consistency_weight = cfg->consistency_weight; + RUN(anchor_consistency_build(msa, ap, cfg->consistency_anchors, + cfg->consistency_weight, (struct consistency_table**)&msa->consistency_table)); } @@ -238,279 +210,61 @@ int kalign_run_seeded(struct msa *msa, int n_threads, int type, } START_TIMER(t1); - if(refine == KALIGN_REFINE_INLINE){ - RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); - }else{ - RUN(create_msa_tree(msa, ap, tasks)); - } - msa->aligned = ALN_STATUS_ALIGNED; - - /* Optional iterative refinement (two-pass approach) */ - if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ - RUN(refine_alignment(msa, ap, tasks, refine)); - } - - /* Free consistency table AFTER refinement */ - if(msa->consistency_table){ - anchor_consistency_free((struct consistency_table*)msa->consistency_table); - msa->consistency_table = NULL; - } - - RUN(finalise_alignment(msa)); - - RUN(msa_sort_rank(msa)); - - STOP_TIMER(t1); - if(!msa->quiet){ - GET_TIMING(t1); - } - DESTROY_TIMER(t1); - - aln_param_free(ap); - free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif - return OK; -ERROR: - if(msa->consistency_table){ - anchor_consistency_free((struct consistency_table*)msa->consistency_table); - msa->consistency_table = NULL; - } - aln_param_free(ap); - free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif - return FAIL; -} - -int kalign_run(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget) -{ - return kalign_run_seeded(msa, n_threads, type, gpo, gpe, tgpe, refine, adaptive_budget, 0, 0.0f, 0.0f, -1.0f, -1.0f, 0, 2.0f); -} - -int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - float use_seq_weights) -{ - struct aln_tasks* tasks = NULL; - struct aln_param* ap = NULL; - RUN(kalign_essential_input_check(msa, 0)); - - if(msa->aligned != ALN_STATUS_UNALIGNED){ - RUN(dealign_msa(msa)); - } - RUN(msa_sort_len_name(msa)); - - if(msa->biotype == ALN_BIOTYPE_DNA){ - msa->L = ALPHA_defDNA; - RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); - }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ - msa->L = ALPHA_redPROTEIN; - RUN(convert_msa_to_internal(msa, ALPHA_redPROTEIN)); - }else{ - ERROR_MSG("Unable to determine what alphabet to use."); - } - - RUN(alloc_tasks(&tasks, msa->numseq)); - -#ifdef USE_THREADPOOL - threadpool_t *pool = tp_create(n_threads); - msa->pool = pool; -#elif defined(HAVE_OPENMP) - omp_set_num_threads(n_threads); -#endif - - RUN(build_tree_kmeans(msa, &tasks)); - - if(msa->biotype == ALN_BIOTYPE_PROTEIN){ - RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); - } - - RUN(resolve_matrix_auto(msa, &type)); - - RUN(aln_param_init(&ap, - msa->biotype, - n_threads, - type, - gpo, - gpe, - tgpe)); -#ifdef USE_THREADPOOL - ap->pool = pool; -#endif - - ap->adaptive_budget = adaptive_budget; - if(use_seq_weights >= 0.0f){ - ap->use_seq_weights = use_seq_weights; - } - ap->dist_scale = dist_scale; - if(vsm_amax >= 0.0f){ - ap->vsm_amax = vsm_amax; - } - - if(ap->use_seq_weights > 0.0f){ - RUN(compute_tree_weights(msa, tasks)); - } - - DECLARE_TIMER(t1); - if(!msa->quiet){ - LOG_MSG("Aligning (dist_scale=%.2f, vsm_amax=%.2f)", dist_scale, vsm_amax); - } - START_TIMER(t1); - - if(refine == KALIGN_REFINE_INLINE){ + /* Initial alignment */ + if(cfg->refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); } msa->aligned = ALN_STATUS_ALIGNED; - if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ - RUN(refine_alignment(msa, ap, tasks, refine)); - } - - RUN(finalise_alignment(msa)); - RUN(msa_sort_rank(msa)); - - STOP_TIMER(t1); - if(!msa->quiet){ - GET_TIMING(t1); - } - DESTROY_TIMER(t1); - - aln_param_free(ap); - free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif - return OK; -ERROR: - aln_param_free(ap); - free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif - return FAIL; -} - -int kalign_run_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights, - int consistency_anchors, float consistency_weight) -{ - struct aln_tasks* tasks = NULL; - struct aln_param* ap = NULL; - int iter; - - RUN(kalign_essential_input_check(msa, 0)); - - if(msa->aligned != ALN_STATUS_UNALIGNED){ - RUN(dealign_msa(msa)); - } - RUN(msa_sort_len_name(msa)); - - if(msa->biotype == ALN_BIOTYPE_DNA){ - msa->L = ALPHA_defDNA; - RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); - }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ - msa->L = ALPHA_redPROTEIN; - RUN(convert_msa_to_internal(msa, ALPHA_redPROTEIN)); - }else{ - ERROR_MSG("Unable to determine what alphabet to use."); - } - - RUN(alloc_tasks(&tasks, msa->numseq)); - -#ifdef USE_THREADPOOL - threadpool_t *pool = tp_create(n_threads); - msa->pool = pool; -#elif defined(HAVE_OPENMP) - omp_set_num_threads(n_threads); -#endif - - /* Initial guide tree from BPM anchor distances */ - RUN(build_tree_kmeans(msa, &tasks)); - - if(msa->biotype == ALN_BIOTYPE_PROTEIN){ - RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); - } - - RUN(resolve_matrix_auto(msa, &type)); - - RUN(aln_param_init(&ap, - msa->biotype, - n_threads, - type, - gpo, - gpe, - tgpe)); -#ifdef USE_THREADPOOL - ap->pool = pool; -#endif - - ap->adaptive_budget = adaptive_budget; - if(use_seq_weights >= 0.0f){ - ap->use_seq_weights = use_seq_weights; - } - ap->dist_scale = dist_scale; - if(vsm_amax >= 0.0f){ - ap->vsm_amax = vsm_amax; - } - - if(ap->use_seq_weights > 0.0f){ - RUN(compute_tree_weights(msa, tasks)); - } - - /* Build anchor consistency table if requested */ - if(consistency_anchors > 0){ - ap->consistency_anchors = consistency_anchors; - ap->consistency_weight = consistency_weight; - RUN(anchor_consistency_build(msa, ap, consistency_anchors, - consistency_weight, - (struct consistency_table**)&msa->consistency_table)); - } - - DECLARE_TIMER(t1); - if(!msa->quiet){ - LOG_MSG("Aligning (realign=%d, dist_scale=%.2f, vsm_amax=%.2f)", - realign_iterations, dist_scale, vsm_amax); - } - START_TIMER(t1); - - /* First alignment with BPM-based guide tree */ - if(refine == KALIGN_REFINE_INLINE){ - RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); - }else{ - RUN(create_msa_tree(msa, ap, tasks)); - } - msa->aligned = ALN_STATUS_ALIGNED; - - /* Iterative realignment: align -> compute distances -> new tree -> re-align */ - for(iter = 0; iter < realign_iterations; iter++){ + /* Iterative realignment: align → N×K anchor distances → bisecting + k-means tree → dealign → re-encode → re-align. + Sequences stay aligned until after tree build so the identity + distance callback can read gap characters from seq->seq. */ + for(iter = 0; iter < cfg->realign; iter++){ float** dm = NULL; + int* realign_anchors = NULL; + int n_realign_anchors = 0; int si; - /* Finalize to get character sequences with gap characters */ RUN(finalise_alignment(msa)); - /* Compute NxN pairwise identity distances from alignment */ - RUN(compute_aln_pairwise_dist(msa, &dm)); + /* Select K diverse anchors and compute N×K distances + (sequences still aligned — gap chars in seq->seq). */ + RUN(pick_anchor_from_alignment(msa, REALIGN_NUM_ANCHORS, + &realign_anchors, + &n_realign_anchors)); + RUN(compute_aln_anchor_dist(msa, realign_anchors, + n_realign_anchors, &dm)); + MFREE(realign_anchors); + + /* Rebuild guide tree via bisecting k-means. + The leaf-cluster callback uses identity distances from + the aligned sequences (seq->seq still has gap chars). */ + free_tasks(tasks); + tasks = NULL; + RUN(alloc_tasks(&tasks, msa->numseq)); + RUN(build_tree_kmeans_from_dm(msa, &tasks, dm, + n_realign_anchors, + aln_identity_pair_dist)); + + /* Free N×K matrix (AVX2-aware) */ + { + int fi; + for(fi = 0; fi < msa->numseq; fi++){ + if(dm[fi]){ +#ifdef HAVE_AVX2 + _mm_free(dm[fi]); +#else + MFREE(dm[fi]); +#endif + } + } + MFREE(dm); + } - /* Remove gaps, reset alignment status. - dealign_msa zeroes the gaps[] array but does NOT strip '-' - from seq->seq (which was linearized by finalise_alignment). - We must rebuild seq->seq without gap characters. */ + /* NOW dealign: strip gap characters from seq->seq. */ RUN(dealign_msa(msa)); for(si = 0; si < msa->numseq; si++){ struct msa_seq* seq = msa->sequences[si]; @@ -524,29 +278,21 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, seq->len = w; } - /* Re-encode internal representation for alignment */ + /* Re-encode for alignment */ if(msa->biotype == ALN_BIOTYPE_DNA){ RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); } - /* Reset profile tracking */ RUN(set_sip_nsip(msa)); - /* Rebuild guide tree from alignment-derived distances */ - free_tasks(tasks); - tasks = NULL; - RUN(alloc_tasks(&tasks, msa->numseq)); - RUN(build_tree_from_pairwise(msa, &tasks, dm)); - free_aln_dm(dm, msa->numseq); - if(ap->use_seq_weights > 0.0f){ RUN(compute_tree_weights(msa, tasks)); } /* Re-align with new tree */ - if(refine == KALIGN_REFINE_INLINE){ + if(cfg->refine == KALIGN_REFINE_INLINE){ RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); }else{ RUN(create_msa_tree(msa, ap, tasks)); @@ -554,9 +300,9 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, msa->aligned = ALN_STATUS_ALIGNED; } - /* Refinement after all realign iterations (two-pass, skip for inline) */ - if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ - RUN(refine_alignment(msa, ap, tasks, refine)); + /* Two-pass refinement after the final alignment pass */ + if(cfg->refine != KALIGN_REFINE_NONE && cfg->refine != KALIGN_REFINE_INLINE){ + RUN(refine_alignment(msa, ap, tasks, cfg->refine)); } /* Free consistency table AFTER refinement */ @@ -576,169 +322,43 @@ int kalign_run_realign(struct msa *msa, int n_threads, int type, aln_param_free(ap); free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif return OK; ERROR: - if(msa->consistency_table){ + if(msa && msa->consistency_table){ anchor_consistency_free((struct consistency_table*)msa->consistency_table); msa->consistency_table = NULL; } aln_param_free(ap); - free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif + if(tasks) free_tasks(tasks); return FAIL; } -int kalign_post_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights) +/* Legacy entry point — thin wrapper around kalign_align_full */ +int kalign(char **seq, int *len, int numseq, int n_threads, int type, + float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len) { - struct aln_tasks* tasks = NULL; - struct aln_param* ap = NULL; - int iter; - - ASSERT(msa != NULL, "No MSA"); - ASSERT(realign_iterations > 0, "Need at least 1 realign iteration"); - - /* Detect biotype if not set */ - if(msa->biotype == ALN_BIOTYPE_UNDEF){ - RUN(detect_alphabet(msa)); - } - - /* seq_distances available from prior alignment */ - RUN(resolve_matrix_auto(msa, &type)); - - RUN(aln_param_init(&ap, - msa->biotype, - n_threads, - type, - gpo, - gpe, - tgpe)); -#ifdef USE_THREADPOOL - threadpool_t *pool = tp_create(n_threads); - msa->pool = pool; - ap->pool = pool; -#elif defined(HAVE_OPENMP) - omp_set_num_threads(n_threads); -#endif - ap->adaptive_budget = adaptive_budget; - if(use_seq_weights >= 0.0f){ - ap->use_seq_weights = use_seq_weights; - } - ap->dist_scale = dist_scale; - if(vsm_amax >= 0.0f){ - ap->vsm_amax = vsm_amax; - } - - DECLARE_TIMER(t1); - if(!msa->quiet){ - LOG_MSG("Post-realign (%d iterations, vsm_amax=%.2f)", - realign_iterations, ap->vsm_amax); - } - START_TIMER(t1); - - for(iter = 0; iter < realign_iterations; iter++){ - float** dm = NULL; - int si; - - /* Finalize if not already (first iter may already be FINAL from ensemble) */ - if(msa->aligned != ALN_STATUS_FINAL){ - RUN(finalise_alignment(msa)); - } - - /* Compute NxN pairwise identity distances from alignment */ - RUN(compute_aln_pairwise_dist(msa, &dm)); - - /* Strip gap characters from seq->seq and fix seq->len. - Consensus alignment may have set len to alignment length, - so we recompute from the ungapped sequence. - We also zero gaps[] and reset alignment status manually - (rather than calling dealign_msa which uses the possibly - wrong len to bound the gaps[] loop). */ - for(si = 0; si < msa->numseq; si++){ - struct msa_seq* seq = msa->sequences[si]; - int r, w = 0; - for(r = 0; seq->seq[r] != '\0'; r++){ - if(seq->seq[r] != '-'){ - seq->seq[w++] = seq->seq[r]; - } - } - seq->seq[w] = '\0'; - seq->len = w; - /* Zero gaps array (len+1 entries) */ - for(r = 0; r <= w; r++){ - seq->gaps[r] = 0; - } - } - msa->aligned = ALN_STATUS_UNALIGNED; - - /* Re-encode to internal representation */ - if(msa->biotype == ALN_BIOTYPE_DNA){ - RUN(convert_msa_to_internal(msa, ALPHA_defDNA)); - }else if(msa->biotype == ALN_BIOTYPE_PROTEIN){ - RUN(convert_msa_to_internal(msa, ALPHA_ambigiousPROTEIN)); - } - - /* Reset profile tracking */ - RUN(set_sip_nsip(msa)); - - /* Build UPGMA tree from alignment-derived distances */ - if(tasks){ free_tasks(tasks); tasks = NULL; } - RUN(alloc_tasks(&tasks, msa->numseq)); - RUN(build_tree_from_pairwise(msa, &tasks, dm)); - free_aln_dm(dm, msa->numseq); - - if(ap->use_seq_weights > 0.0f){ - RUN(compute_tree_weights(msa, tasks)); - } - - /* Re-align with new tree */ - if(refine == KALIGN_REFINE_INLINE){ - RUN(create_msa_tree_inline_refine(msa, ap, tasks, 3)); - }else{ - RUN(create_msa_tree(msa, ap, tasks)); - } - msa->aligned = ALN_STATUS_ALIGNED; - } + struct msa *msa = NULL; + struct kalign_run_config cfg; - /* Refinement after all realign iterations (two-pass, skip for inline) */ - if(refine != KALIGN_REFINE_NONE && refine != KALIGN_REFINE_INLINE){ - RUN(refine_alignment(msa, ap, tasks, refine)); + RUN(kalign_arr_to_msa(seq, len, numseq, &msa)); + msa->quiet = 1; + if(n_threads < 1){ + n_threads = 1; } - RUN(finalise_alignment(msa)); - RUN(msa_sort_rank(msa)); + cfg = kalign_run_config_defaults(); + cfg.matrix = type; + cfg.gpo = gpo; + cfg.gpe = gpe; + cfg.tgpe = tgpe; - STOP_TIMER(t1); - if(!msa->quiet){ - GET_TIMING(t1); - } - DESTROY_TIMER(t1); + RUN(kalign_align_full(msa, &cfg, 1, NULL, n_threads)); - aln_param_free(ap); - free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif + RUN(kalign_msa_to_arr(msa, aligned, out_aln_len)); + kalign_free_msa(msa); return OK; ERROR: - aln_param_free(ap); - if(tasks) free_tasks(tasks); -#ifdef USE_THREADPOOL - msa->pool = NULL; - tp_destroy(pool); -#endif + if(msa) kalign_free_msa(msa); return FAIL; } @@ -781,38 +401,39 @@ int kalign_align_full(struct msa* msa, const struct kalign_ensemble_config* ens, int n_threads) { +#ifdef USE_THREADPOOL + threadpool_t *pool = NULL; +#endif + ASSERT(msa != NULL, "No MSA"); ASSERT(runs != NULL, "No run configs"); ASSERT(n_runs >= 1, "n_runs must be >= 1"); + if(n_threads < 1) n_threads = 1; + +#ifdef USE_THREADPOOL + pool = tp_create(n_threads); + msa->pool = pool; +#elif defined(HAVE_OPENMP) + omp_set_num_threads(n_threads); +#endif + if(n_runs > 1){ - /* Ensemble path */ RUN(kalign_ensemble_from_configs(msa, runs, n_runs, ens, n_threads)); }else{ - /* Single-run path */ - const struct kalign_run_config* r = &runs[0]; - if(r->realign > 0){ - RUN(kalign_run_realign(msa, n_threads, r->matrix, - r->gpo, r->gpe, r->tgpe, - r->refine, r->adaptive_budget, - r->dist_scale, r->vsm_amax, - r->realign, r->seq_weights, - r->consistency_anchors, - r->consistency_weight)); - }else{ - RUN(kalign_run_seeded(msa, n_threads, r->matrix, - r->gpo, r->gpe, r->tgpe, - r->refine, r->adaptive_budget, - r->tree_seed, r->tree_noise, - r->dist_scale, r->vsm_amax, - r->seq_weights, - r->consistency_anchors, - r->consistency_weight)); - } + RUN(kalign_single_run(msa, &runs[0], n_threads)); } +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return OK; ERROR: +#ifdef USE_THREADPOOL + msa->pool = NULL; + tp_destroy(pool); +#endif return FAIL; } diff --git a/lib/src/aln_wrap.h b/lib/src/aln_wrap.h index 97452d5..fbaa66a 100644 --- a/lib/src/aln_wrap.h +++ b/lib/src/aln_wrap.h @@ -18,34 +18,12 @@ struct msa; -EXTERN int kalign_run(struct msa *msa, int n_threads, int type, float gpo, float gpe, float tgpe, int refine, int adaptive_budget); -EXTERN int kalign_run_seeded(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - uint64_t tree_seed, float tree_noise, - float dist_scale, float vsm_amax, - float use_seq_weights, - int consistency_anchors, float consistency_weight); -EXTERN int kalign_run_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights, - int consistency_anchors, float consistency_weight); - -EXTERN int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - float use_seq_weights); - -EXTERN int kalign_post_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights); +/* Internal single-run entry point. Caller must set msa->pool before + calling (if USE_THREADPOOL is enabled). Does NOT create or destroy + the threadpool. Used by kalign_align_full and ensemble.c. */ +EXTERN int kalign_single_run(struct msa *msa, + const struct kalign_run_config *cfg, + int n_threads); EXTERN struct kalign_run_config kalign_run_config_defaults(void); EXTERN struct kalign_ensemble_config kalign_ensemble_config_defaults(void); diff --git a/lib/src/anchor_consistency.c b/lib/src/anchor_consistency.c index 43fa678..0a769ab 100644 --- a/lib/src/anchor_consistency.c +++ b/lib/src/anchor_consistency.c @@ -10,6 +10,10 @@ #include "aln_setup.h" #include "aln_controller.h" +#ifdef USE_THREADPOOL +#include "threadpool/threadpool.h" +#endif + #define ANCHOR_CONSISTENCY_IMPORT #include "anchor_consistency.h" @@ -197,6 +201,87 @@ static int select_anchors(struct msa* msa, int K, int* anchor_ids) return FAIL; } +/* Serial build: each sequence i aligns against all K anchors */ +static int anchor_build_serial(struct msa* msa, struct aln_param* ap, + struct consistency_table* ct) +{ + int N = ct->numseq; + int K = ct->n_anchors; + int i, k; + + for(i = 0; i < N; i++){ + int len_i = msa->sequences[i]->len; + for(k = 0; k < K; k++){ + int ak = ct->anchor_ids[k]; + ct->map_lengths[i * K + k] = len_i; + + if(i == ak){ + int p; + MMALLOC(ct->pos_maps[i * K + k], sizeof(int) * len_i); + for(p = 0; p < len_i; p++){ + ct->pos_maps[i * K + k][p] = p; + } + }else{ + RUN(pairwise_align_map(ap, + msa->sequences[i]->s, len_i, + msa->sequences[ak]->s, msa->sequences[ak]->len, + &ct->pos_maps[i * K + k])); + } + } + } + return OK; +ERROR: + return FAIL; +} + +#ifdef USE_THREADPOOL +/* Context for parallel anchor build */ +struct anchor_par_ctx { + struct msa* msa; + struct aln_param* ap; + struct consistency_table* ct; + int error; /* set non-zero on failure */ +}; + +static void anchor_build_chunk(int start, int end, void* arg) +{ + struct anchor_par_ctx* ctx = (struct anchor_par_ctx*)arg; + struct msa* msa = ctx->msa; + struct aln_param* ap = ctx->ap; + struct consistency_table* ct = ctx->ct; + int K = ct->n_anchors; + int i, k; + + for(i = start; i < end; i++){ + int len_i = msa->sequences[i]->len; + for(k = 0; k < K; k++){ + int ak = ct->anchor_ids[k]; + ct->map_lengths[i * K + k] = len_i; + + if(i == ak){ + /* Identity map — no allocation can fail in practice + but we must handle it gracefully */ + int* map = malloc(sizeof(int) * len_i); + if(!map){ ctx->error = 1; return; } + for(int p = 0; p < len_i; p++){ + map[p] = p; + } + ct->pos_maps[i * K + k] = map; + }else{ + /* Pairwise alignment: each thread does its own DP */ + if(pairwise_align_map(ap, + msa->sequences[i]->s, len_i, + msa->sequences[ak]->s, msa->sequences[ak]->len, + &ct->pos_maps[i * K + k]) != OK){ + ctx->error = 1; + return; + } + } + } + } +} +#endif /* USE_THREADPOOL */ + int anchor_consistency_build(struct msa* msa, struct aln_param* ap, int n_anchors, float weight, struct consistency_table** ct_out) @@ -204,7 +289,7 @@ int anchor_consistency_build(struct msa* msa, struct aln_param* ap, struct consistency_table* ct = NULL; int N = msa->numseq; int K = n_anchors; - int i, k; + int i; if(K <= 0 || N < 3){ *ct_out = NULL; @@ -242,29 +327,25 @@ int anchor_consistency_build(struct msa* msa, struct aln_param* ap, LOG_MSG("Anchor consistency: K=%d, weight=%.1f", K, weight); } - /* Build position maps: for each sequence i, for each anchor k */ - for(i = 0; i < N; i++){ - int len_i = msa->sequences[i]->len; - for(k = 0; k < K; k++){ - int ak = ct->anchor_ids[k]; - ct->map_lengths[i * K + k] = len_i; - - if(i == ak){ - /* Identity map for anchor itself */ - int p; - MMALLOC(ct->pos_maps[i * K + k], sizeof(int) * len_i); - for(p = 0; p < len_i; p++){ - ct->pos_maps[i * K + k][p] = p; - } - }else{ - /* Pairwise alignment: seq_i vs anchor_k */ - RUN(pairwise_align_map(ap, - msa->sequences[i]->s, len_i, - msa->sequences[ak]->s, msa->sequences[ak]->len, - &ct->pos_maps[i * K + k])); - } + /* Build position maps: for each sequence i, for each anchor k. + This is the main bottleneck — O(N*K) pairwise DP alignments. */ +#ifdef USE_THREADPOOL + if(msa->pool != NULL){ + struct anchor_par_ctx ctx; + ctx.msa = msa; + ctx.ap = ap; + ctx.ct = ct; + ctx.error = 0; + tp_parallel_for(msa->pool, 0, N, anchor_build_chunk, &ctx); + if(ctx.error){ + ERROR_MSG("Parallel anchor build failed"); } + }else{ + RUN(anchor_build_serial(msa, ap, ct)); } +#else + RUN(anchor_build_serial(msa, ap, ct)); +#endif *ct_out = ct; return OK; diff --git a/lib/src/bisectingKmeans.c b/lib/src/bisectingKmeans.c index 0983ef3..081c33b 100644 --- a/lib/src/bisectingKmeans.c +++ b/lib/src/bisectingKmeans.c @@ -54,7 +54,8 @@ static void create_tasks(struct node*n, struct aln_tasks* t); /* static int bisecting_kmeans_serial(struct msa *msa, struct node **ret_n, float **dm, int *samples, int num_samples); */ static int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * dm, - int* samples, int num_samples); + int* samples, int num_samples, + pair_dist_fn leaf_dist); /* static int bisecting_kmeans_parallel(struct msa* msa, struct node** ret_n, float** dm,int* samples, int num_samples); */ static int split(const float * const * dm, int *samples, int num_anchors, int num_samples, @@ -84,12 +85,13 @@ struct bisect_task_arg { const float *const *dm; int *samples; int num_samples; + pair_dist_fn leaf_dist; }; static void bisect_task_fn(void *arg) { struct bisect_task_arg *a = (struct bisect_task_arg *)arg; - bisecting_kmeans(a->msa, a->ret_n, a->dm, a->samples, a->num_samples); + bisecting_kmeans(a->msa, a->ret_n, a->dm, a->samples, a->num_samples, a->leaf_dist); } #endif @@ -107,6 +109,11 @@ inline int cmp_floats(const float a, const float b) } } +/* Default leaf-cluster distance: BPM on raw internal sequences */ +static float** bpm_pair_dist(struct msa* msa, int* samples, int n) +{ + return d_estimation(msa, samples, n, 1); +} int build_tree_kmeans_noisy(struct msa* msa, struct aln_tasks** tasks, uint64_t seed, float noise_sigma) @@ -172,7 +179,7 @@ int build_tree_kmeans_noisy(struct msa* msa, struct aln_tasks** tasks, #pragma omp single nowait #endif #endif - bisecting_kmeans(msa, &root, (const float * const *)dm, samples, numseq); + bisecting_kmeans(msa, &root, (const float * const *)dm, samples, numseq, bpm_pair_dist); STOP_TIMER(timer); if(!msa->quiet){ @@ -267,7 +274,7 @@ int build_tree_kmeans(struct msa* msa, struct aln_tasks** tasks) #pragma omp single nowait #endif #endif - bisecting_kmeans(msa,&root, (const float * const *)dm, samples, numseq); + bisecting_kmeans(msa,&root, (const float * const *)dm, samples, numseq, bpm_pair_dist); /* } */ STOP_TIMER(timer); @@ -309,7 +316,8 @@ int build_tree_kmeans(struct msa* msa, struct aln_tasks** tasks) return FAIL; } -int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * dm,int* samples, int num_samples) +int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * dm, + int* samples, int num_samples, pair_dist_fn leaf_dist) { struct kmeans_result* res_tmp = NULL; struct kmeans_result* best = NULL; @@ -348,7 +356,7 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * if(threshold < 3) threshold = 3; if(num_samples < threshold){ float** dm_local = NULL; - RUNP(dm_local = d_estimation(msa, samples, num_samples,1)); + RUNP(dm_local = leaf_dist(msa, samples, num_samples)); n = upgma(dm_local, samples, num_samples); *ret_n = n; gfree(dm_local); @@ -460,8 +468,8 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * #ifdef USE_THREADPOOL { - struct bisect_task_arg left_arg = { msa, &n->left, dm, sl, num_l }; - struct bisect_task_arg right_arg = { msa, &n->right, dm, sr, num_r }; + struct bisect_task_arg left_arg = { msa, &n->left, dm, sl, num_l, leaf_dist }; + struct bisect_task_arg right_arg = { msa, &n->right, dm, sr, num_r, leaf_dist }; tp_group_t *g = tp_group_create(msa->pool); tp_group_submit(g, bisect_task_fn, &left_arg); tp_group_submit(g, bisect_task_fn, &right_arg); @@ -472,12 +480,12 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * #ifdef HAVE_OPENMP #pragma omp task shared(msa,n,dm) #endif - bisecting_kmeans(msa,&n->left, dm, sl, num_l); + bisecting_kmeans(msa,&n->left, dm, sl, num_l, leaf_dist); #ifdef HAVE_OPENMP #pragma omp task shared(msa,n,dm,num_anchors) #endif - bisecting_kmeans(msa,&n->right, dm, sr, num_r); + bisecting_kmeans(msa,&n->right, dm, sr, num_r, leaf_dist); #ifdef HAVE_OPENMP #pragma omp taskwait @@ -1232,6 +1240,75 @@ void free_kmeans_results(struct kmeans_result* k) } } +/* Build guide tree from a pre-computed N×K distance matrix using bisecting + k-means. Used by the realign loop where distances are computed from aligned + sequences (identity distances, already 0..1) rather than BPM on raw sequences. + + The caller owns dm and is responsible for freeing it. This function does NOT + free dm — unlike build_tree_kmeans which computes and frees its own dm. */ +int build_tree_kmeans_from_dm(struct msa* msa, struct aln_tasks** tasks, + float** dm, int num_anchors, + pair_dist_fn leaf_dist) +{ + struct aln_tasks* t = NULL; + struct node* root = NULL; + int* samples = NULL; + int numseq; + int i; + + ASSERT(msa != NULL, "No alignment."); + ASSERT(dm != NULL, "No distance matrix."); + ASSERT(num_anchors > 0, "num_anchors must be > 0"); + + t = *tasks; + if(!t){ + RUN(alloc_tasks(&t, msa->numseq)); + } + numseq = msa->numseq; + + MMALLOC(samples, sizeof(int) * numseq); + for(i = 0; i < numseq; i++){ + samples[i] = i; + } + + /* Bisecting k-means — same parallel algorithm as initial tree. + Note: bisecting_kmeans() takes ownership of samples and frees it. */ +#if !defined(USE_THREADPOOL) +#ifdef HAVE_OPENMP +#pragma omp parallel +#pragma omp single nowait +#endif +#endif + bisecting_kmeans(msa, &root, (const float * const *)dm, samples, numseq, leaf_dist); + samples = NULL; /* owned and freed by bisecting_kmeans */ + ASSERT(root != NULL, "Bisecting k-means tree construction failed."); + + label_internal(root, numseq); + create_tasks(root, t); + + /* Compute per-sequence mean distance to anchors. + Identity distances are already 0..1, so NO length normalization + (unlike BPM distances in build_tree_kmeans which divides by seq_len). */ + if(msa->seq_distances == NULL){ + MMALLOC(msa->seq_distances, sizeof(float) * numseq); + } + for(i = 0; i < numseq; i++){ + float sum = 0.0f; + int j; + for(j = 0; j < num_anchors; j++){ + sum += dm[i][j]; + } + msa->seq_distances[i] = sum / (float)num_anchors; + } + + MFREE(root); + *tasks = t; + return OK; +ERROR: + if(samples) MFREE(samples); + return FAIL; +} + int build_tree_from_pairwise(struct msa* msa, struct aln_tasks** tasks, float** dm) { struct aln_tasks* t = NULL; diff --git a/lib/src/bisectingKmeans.h b/lib/src/bisectingKmeans.h index 3d3d861..07188ac 100644 --- a/lib/src/bisectingKmeans.h +++ b/lib/src/bisectingKmeans.h @@ -3,7 +3,6 @@ #include - #ifdef BISECTINGKMEANS_IMPORT #define EXTERN #else @@ -14,16 +13,21 @@ #endif #endif - - - struct aln_tasks; struct msa; -/* int build_tree_kmeans(struct msa* msa, struct aln_param* ap,struct aln_tasks** task_list); */ + +/* Callback for computing pairwise distances within a leaf cluster. + Given a subset of sequence indices (samples[0..n-1]), return an n×n + symmetric distance matrix. The caller (UPGMA) frees it via gfree(). + Returns NULL on failure. */ +typedef float** (*pair_dist_fn)(struct msa* msa, int* samples, int n); EXTERN int build_tree_kmeans(struct msa* msa, struct aln_tasks** tasks); EXTERN int build_tree_kmeans_noisy(struct msa* msa, struct aln_tasks** tasks, uint64_t seed, float noise_sigma); +EXTERN int build_tree_kmeans_from_dm(struct msa* msa, struct aln_tasks** tasks, + float** dm, int num_anchors, + pair_dist_fn leaf_dist); EXTERN int build_tree_from_pairwise(struct msa* msa, struct aln_tasks** tasks, float** dm); diff --git a/lib/src/consensus_msa.c b/lib/src/consensus_msa.c index faad1f3..cf19431 100644 --- a/lib/src/consensus_msa.c +++ b/lib/src/consensus_msa.c @@ -6,6 +6,10 @@ #include "msa_alloc.h" #include "poar.h" +#ifdef USE_THREADPOOL +#include "threadpool/threadpool.h" +#endif + #define CONSENSUS_MSA_IMPORT #include "consensus_msa.h" @@ -369,10 +373,72 @@ static int topo_sort(int* col_id, /* [total_residues]: element -> column return FAIL; } +#ifdef USE_THREADPOOL +struct cand_count_ctx { + struct poar_table* table; + int numseq; + int min_support; + int* row_counts; +}; + +static void cand_count_chunk(int start, int end, void* arg) +{ + struct cand_count_ctx* c = (struct cand_count_ctx*)arg; + for(int i = start; i < end; i++){ + int count = 0; + for(int j = i + 1; j < c->numseq; j++){ + int pidx = pair_index(i, j, c->numseq); + struct poar_pair* pp = c->table->pairs[pidx]; + for(int e = 0; e < pp->n_entries; e++){ + if(popcount32(pp->entries[e].support) >= c->min_support){ + count++; + } + } + } + c->row_counts[i] = count; + } +} + +struct cand_fill_ctx { + struct poar_table* table; + int numseq; + int min_support; + int* seq_offsets; + int* row_offsets; + struct merge_candidate* candidates; +}; + +static void cand_fill_chunk(int start, int end, void* arg) +{ + struct cand_fill_ctx* c = (struct cand_fill_ctx*)arg; + for(int i = start; i < end; i++){ + int pos = c->row_offsets[i]; + for(int j = i + 1; j < c->numseq; j++){ + int pidx = pair_index(i, j, c->numseq); + struct poar_pair* pp = c->table->pairs[pidx]; + for(int e = 0; e < pp->n_entries; e++){ + int support = popcount32(pp->entries[e].support); + if(support >= c->min_support){ + uint32_t key = pp->entries[e].key; + c->candidates[pos].elem_i = c->seq_offsets[i] + (int)(key >> 20); + c->candidates[pos].elem_j = c->seq_offsets[j] + (int)(key & 0xFFFFF); + c->candidates[pos].support = support; + pos++; + } + } + } + } +} +#endif + int build_consensus(struct poar_table* table, int* seq_lengths, int numseq, int min_support, - struct msa* out_msa) + struct msa* out_msa +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { struct uf_set* uf = NULL; int* seq_offsets = NULL; @@ -409,28 +475,64 @@ int build_consensus(struct poar_table* table, in descending support order so higher-confidence pairs merge first */ { int n_candidates = 0; - int alloc_candidates = 1024; struct merge_candidate *candidates = NULL; - MMALLOC(candidates, sizeof(*candidates) * alloc_candidates); - - for(i = 0; i < numseq - 1; i++){ - for(j = i + 1; j < numseq; j++){ - int pidx = pair_index(i, j, numseq); - struct poar_pair* pp = table->pairs[pidx]; +#ifdef USE_THREADPOOL + if(pool != NULL && numseq > 16){ + /* Two-pass parallel: count then fill */ + int* row_counts = NULL; + int* row_offsets = NULL; + MMALLOC(row_counts, sizeof(int) * numseq); + MMALLOC(row_offsets, sizeof(int) * numseq); + + struct cand_count_ctx cc = { table, numseq, min_support, row_counts }; + tp_parallel_for(pool, 0, numseq - 1, cand_count_chunk, &cc); + row_counts[numseq - 1] = 0; + + /* Prefix sum for offsets */ + row_offsets[0] = 0; + for(i = 1; i < numseq; i++){ + row_offsets[i] = row_offsets[i-1] + row_counts[i-1]; + } + n_candidates = row_offsets[numseq-1] + row_counts[numseq-1]; + + if(n_candidates > 0){ + MMALLOC(candidates, sizeof(*candidates) * n_candidates); + struct cand_fill_ctx cf = { + table, numseq, min_support, + seq_offsets, row_offsets, candidates + }; + tp_parallel_for(pool, 0, numseq - 1, cand_fill_chunk, &cf); + }else{ + MMALLOC(candidates, sizeof(*candidates) * 1); + } - for(int e = 0; e < pp->n_entries; e++){ - int support = popcount32(pp->entries[e].support); - if(support >= min_support){ - if(n_candidates >= alloc_candidates){ - alloc_candidates *= 2; - MREALLOC(candidates, sizeof(*candidates) * alloc_candidates); + MFREE(row_counts); + MFREE(row_offsets); + }else +#endif + { + int alloc_candidates = 1024; + MMALLOC(candidates, sizeof(*candidates) * alloc_candidates); + + for(i = 0; i < numseq - 1; i++){ + for(j = i + 1; j < numseq; j++){ + int pidx = pair_index(i, j, numseq); + struct poar_pair* pp = table->pairs[pidx]; + + for(int e = 0; e < pp->n_entries; e++){ + int support = popcount32(pp->entries[e].support); + if(support >= min_support){ + if(n_candidates >= alloc_candidates){ + alloc_candidates *= 2; + MREALLOC(candidates, sizeof(*candidates) * alloc_candidates); + } + uint32_t key = pp->entries[e].key; + candidates[n_candidates].elem_i = seq_offsets[i] + (int)(key >> 20); + candidates[n_candidates].elem_j = seq_offsets[j] + (int)(key & 0xFFFFF); + candidates[n_candidates].support = support; + n_candidates++; } - uint32_t key = pp->entries[e].key; - candidates[n_candidates].elem_i = seq_offsets[i] + (int)(key >> 20); - candidates[n_candidates].elem_j = seq_offsets[j] + (int)(key & 0xFFFFF); - candidates[n_candidates].support = support; - n_candidates++; } } } @@ -561,8 +663,69 @@ int build_consensus(struct poar_table* table, - confidence = sum(supports) / (n_residue_pairs * n_alignments) Gaps get confidence 0.0. Column confidence = mean of residue confidences in that column. */ +#ifdef USE_THREADPOOL +struct confidence_ctx { + struct poar_table* table; + struct pos_matrix* pm; + struct msa* msa; + int numseq; + int alnlen; + int n_alignments; +}; + +static void confidence_chunk(int start, int end, void* arg) +{ + struct confidence_ctx* c = (struct confidence_ctx*)arg; + int j, col; + for(int i = start; i < end; i++){ + for(col = 0; col < c->alnlen; col++){ + int ri = c->pm->col_to_res[i][col]; + if(ri < 0){ + c->msa->sequences[i]->confidence[col] = 0.0f; + continue; + } + double sum_support = 0.0; + int n_pairs = 0; + for(j = 0; j < c->numseq; j++){ + if(j == i) continue; + int rj = c->pm->col_to_res[j][col]; + if(rj < 0) continue; + int si = i < j ? i : j; + int sj = i < j ? j : i; + int pidx = pair_index(si, sj, c->numseq); + struct poar_pair* pp = c->table->pairs[pidx]; + int orig_i = i < j ? ri : rj; + int orig_j = i < j ? rj : ri; + uint32_t key = ((uint32_t)orig_i << 20) | (uint32_t)orig_j; + int lo = 0, hi = pp->n_entries, support = 0; + while(lo < hi){ + int mid = lo + (hi - lo) / 2; + if(pp->entries[mid].key < key) lo = mid + 1; + else if(pp->entries[mid].key == key){ + support = popcount32(pp->entries[mid].support); + break; + }else hi = mid; + } + sum_support += (double)support; + n_pairs++; + } + if(n_pairs > 0 && c->n_alignments > 0){ + c->msa->sequences[i]->confidence[col] = + (float)(sum_support / ((double)n_pairs * (double)c->n_alignments)); + }else{ + c->msa->sequences[i]->confidence[col] = 0.0f; + } + } + } +} +#endif + int compute_residue_confidence(struct poar_table* table, - struct msa* aligned_msa) + struct msa* aligned_msa +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { struct pos_matrix* pm = NULL; int numseq = aligned_msa->numseq; @@ -603,6 +766,15 @@ int compute_residue_confidence(struct poar_table* table, MMALLOC(aligned_msa->col_confidence, sizeof(float) * alnlen); /* Compute per-residue confidence */ +#ifdef USE_THREADPOOL + if(pool != NULL && numseq > 16){ + struct confidence_ctx ctx = { + table, pm, aligned_msa, numseq, alnlen, n_alignments + }; + tp_parallel_for(pool, 0, numseq, confidence_chunk, &ctx); + }else +#endif + { for(i = 0; i < numseq; i++){ for(col = 0; col < alnlen; col++){ int ri = pm->col_to_res[i][col]; @@ -658,6 +830,7 @@ int compute_residue_confidence(struct poar_table* table, } } } + } /* end serial fallback */ /* Compute per-column confidence: mean over non-gap residues */ for(col = 0; col < alnlen; col++){ @@ -691,45 +864,108 @@ int compute_residue_confidence(struct poar_table* table, agreeing. Summing these gives expected correct pairs. This rewards both high recall (many pairs) and high precision (pairs with broad agreement). */ +#ifdef USE_THREADPOOL +struct score_poar_ctx { + struct poar_table* table; + struct pos_matrix* pm; + int numseq; + int alnlen; + double denom; + double* row_scores; +}; + +static void score_poar_chunk(int start, int end, void* arg) +{ + struct score_poar_ctx* c = (struct score_poar_ctx*)arg; + int j, col; + for(int i = start; i < end; i++){ + double row_sum = 0.0; + for(j = i + 1; j < c->numseq; j++){ + int pidx = pair_index(i, j, c->numseq); + struct poar_pair* pp = c->table->pairs[pidx]; + for(col = 0; col < c->alnlen; col++){ + int ri = c->pm->col_to_res[i][col]; + int rj = c->pm->col_to_res[j][col]; + if(ri >= 0 && rj >= 0){ + uint32_t key = ((uint32_t)ri << 20) | (uint32_t)rj; + int lo = 0, hi = pp->n_entries, support = 0; + while(lo < hi){ + int mid = lo + (hi - lo) / 2; + if(pp->entries[mid].key < key) lo = mid + 1; + else if(pp->entries[mid].key == key){ + support = popcount32(pp->entries[mid].support); + break; + }else hi = mid; + } + row_sum += (double)(support - 1) / c->denom; + } + } + } + c->row_scores[i] = row_sum; + } +} +#endif + int score_alignment_poar(struct poar_table* table, struct pos_matrix* pm, int numseq, int n_alignments, - double* out_score) + double* out_score +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { double total_score = 0.0; - int i, j, col; - int alnlen = pm->alnlen; + int i; double denom = (n_alignments > 1) ? (double)(n_alignments - 1) : 1.0; - for(i = 0; i < numseq - 1; i++){ - for(j = i + 1; j < numseq; j++){ - int pidx = pair_index(i, j, numseq); - struct poar_pair* pp = table->pairs[pidx]; +#ifdef USE_THREADPOOL + if(pool != NULL && numseq > 16){ + double* row_scores = NULL; + MMALLOC(row_scores, sizeof(double) * numseq); + for(i = 0; i < numseq; i++) row_scores[i] = 0.0; - for(col = 0; col < alnlen; col++){ - int ri = pm->col_to_res[i][col]; - int rj = pm->col_to_res[j][col]; - if(ri >= 0 && rj >= 0){ - uint32_t key = ((uint32_t)ri << 20) | (uint32_t)rj; + struct score_poar_ctx ctx = { + table, pm, numseq, pm->alnlen, denom, row_scores + }; + tp_parallel_for(pool, 0, numseq - 1, score_poar_chunk, &ctx); - /* Binary search in sorted entries */ - int lo = 0; - int hi = pp->n_entries; - int support = 0; - while(lo < hi){ - int mid = lo + (hi - lo) / 2; - if(pp->entries[mid].key < key){ - lo = mid + 1; - }else if(pp->entries[mid].key == key){ - support = popcount32(pp->entries[mid].support); - break; - }else{ - hi = mid; + for(i = 0; i < numseq - 1; i++){ + total_score += row_scores[i]; + } + MFREE(row_scores); + + *out_score = total_score; + return OK; + ERROR: + if(row_scores) MFREE(row_scores); + return FAIL; + } +#endif + { + int j, col; + int alnlen = pm->alnlen; + for(i = 0; i < numseq - 1; i++){ + for(j = i + 1; j < numseq; j++){ + int pidx = pair_index(i, j, numseq); + struct poar_pair* pp = table->pairs[pidx]; + for(col = 0; col < alnlen; col++){ + int ri = pm->col_to_res[i][col]; + int rj = pm->col_to_res[j][col]; + if(ri >= 0 && rj >= 0){ + uint32_t key = ((uint32_t)ri << 20) | (uint32_t)rj; + int lo = 0, hi = pp->n_entries, support = 0; + while(lo < hi){ + int mid = lo + (hi - lo) / 2; + if(pp->entries[mid].key < key) lo = mid + 1; + else if(pp->entries[mid].key == key){ + support = popcount32(pp->entries[mid].support); + break; + }else hi = mid; } + total_score += (double)(support - 1) / denom; } - /* support includes self; subtract 1 for other-agreement */ - total_score += (double)(support - 1) / denom; } } } diff --git a/lib/src/consensus_msa.h b/lib/src/consensus_msa.h index 23fc70b..dd36ee5 100644 --- a/lib/src/consensus_msa.h +++ b/lib/src/consensus_msa.h @@ -15,6 +15,27 @@ struct poar_table; struct pos_matrix; struct msa; +#ifdef USE_THREADPOOL +typedef struct threadpool threadpool_t; + +EXTERN int build_consensus(struct poar_table* table, + int* seq_lengths, int numseq, + int min_support, + struct msa* out_msa, + threadpool_t* pool); + +EXTERN int score_alignment_poar(struct poar_table* table, + struct pos_matrix* pm, + int numseq, + int n_alignments, + double* out_score, + threadpool_t* pool); + +EXTERN int compute_residue_confidence(struct poar_table* table, + struct msa* aligned_msa, + threadpool_t* pool); +#else + EXTERN int build_consensus(struct poar_table* table, int* seq_lengths, int numseq, int min_support, @@ -28,6 +49,7 @@ EXTERN int score_alignment_poar(struct poar_table* table, EXTERN int compute_residue_confidence(struct poar_table* table, struct msa* aligned_msa); +#endif #undef CONSENSUS_MSA_IMPORT #undef EXTERN diff --git a/lib/src/ensemble.c b/lib/src/ensemble.c index cf06362..ec97d5a 100644 --- a/lib/src/ensemble.c +++ b/lib/src/ensemble.c @@ -48,36 +48,6 @@ static struct ensemble_params run_params[] = { }; #define N_RUN_PARAMS 12 -/* --------------------------------------------------------------------------- - * Helper: resolve_run_params - * - * Given base gap penalties and a run index, compute the run-specific - * gap-open, gap-extend, terminal-gap-extend, seed, and noise values. - * Run 0 always uses defaults (deterministic, no noise). - * ------------------------------------------------------------------------- */ -static void resolve_run_params(float base_gpo, float base_gpe, float base_tgpe, - int k, uint64_t seed, - float* out_gpo, float* out_gpe, float* out_tgpe, - uint64_t* out_seed, float* out_noise) -{ - if(k == 0){ - /* Run 0: default params, deterministic */ - *out_gpo = base_gpo; - *out_gpe = base_gpe; - *out_tgpe = base_tgpe; - *out_seed = 0; - *out_noise = 0.0f; - }else{ - /* Subsequent runs: independent per-penalty scaling + tree noise */ - struct ensemble_params ep = run_params[k % N_RUN_PARAMS]; - *out_gpo = base_gpo * ep.gpo_scale; - *out_gpe = base_gpe * ep.gpe_scale; - *out_tgpe = base_tgpe * ep.tgpe_scale; - *out_seed = seed + (uint64_t)k; - *out_noise = ep.noise; - } -} - /* --------------------------------------------------------------------------- * Helper: score_alignments * @@ -88,7 +58,11 @@ static void resolve_run_params(float base_gpo, float base_gpe, float base_tgpe, static int score_alignments(struct msa** alignments, struct poar_table* poar, int numseq, int n_runs, int quiet, - double** out_scores, int* out_best_k) + double** out_scores, int* out_best_k +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { struct pos_matrix* pm = NULL; double* scores = NULL; @@ -105,7 +79,11 @@ static int score_alignments(struct msa** alignments, } RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, alignments[k]->alnlen)); +#ifdef USE_THREADPOOL + RUN(score_alignment_poar(poar, pm, numseq, n_runs, &scores[k], pool)); +#else RUN(score_alignment_poar(poar, pm, numseq, n_runs, &scores[k])); +#endif if(!quiet){ LOG_MSG(" Run %d score: %.1f", k + 1, scores[k]); @@ -145,7 +123,11 @@ static int score_alignments(struct msa** alignments, static int build_consensus_from_poar(struct poar_table* poar, struct msa* msa, int numseq, int min_support, - struct msa** out_consensus) + struct msa** out_consensus +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { struct msa* consensus_msa = NULL; int* seq_lens = NULL; @@ -157,7 +139,11 @@ static int build_consensus_from_poar(struct poar_table* poar, seq_lens[i] = msa->sequences[i]->len; } +#ifdef USE_THREADPOOL + RUN(build_consensus(poar, seq_lens, numseq, min_support, consensus_msa, pool)); +#else RUN(build_consensus(poar, seq_lens, numseq, min_support, consensus_msa)); +#endif MFREE(seq_lens); *out_consensus = consensus_msa; @@ -199,7 +185,11 @@ static int copy_alignment_to_msa(struct msa* dst, struct msa* src, int numseq) * out_score. This avoids repeating the aln_seqs + pos_matrix pattern. * ------------------------------------------------------------------------- */ static int score_single_msa(struct msa* aln, struct poar_table* poar, - int numseq, int n_runs, double* out_score) + int numseq, int n_runs, double* out_score +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { struct pos_matrix* pm = NULL; char** aln_seqs = NULL; @@ -210,7 +200,11 @@ static int score_single_msa(struct msa* aln, struct poar_table* poar, } RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, aln->alnlen)); +#ifdef USE_THREADPOOL + RUN(score_alignment_poar(poar, pm, numseq, n_runs, out_score, pool)); +#else RUN(score_alignment_poar(poar, pm, numseq, n_runs, out_score)); +#endif pos_matrix_free(pm); MFREE(aln_seqs); @@ -221,534 +215,31 @@ static int score_single_msa(struct msa* aln, struct poar_table* poar, return FAIL; } -/* ======================================================================== */ - -int kalign_ensemble(struct msa* msa, int n_threads, int type, - int n_runs, float gpo, float gpe, float tgpe, - uint64_t seed, int min_support, - const char* save_poar_path, - int refine, float dist_scale, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight) -{ - struct msa* copy = NULL; - struct msa* consensus_msa = NULL; - struct msa** alignments = NULL; - struct poar_table* poar = NULL; - struct pos_matrix* pm = NULL; - struct aln_param* ap = NULL; - double* scores = NULL; - int numseq; - int k; - int best_k = 0; - int use_consensus = 0; - float base_gpo, base_gpe, base_tgpe; - - ASSERT(msa != NULL, "No MSA"); - ASSERT(n_runs >= 1, "n_runs must be >= 1"); - - /* Seq_weights hurts ensemble performance (POAR consensus already - handles profile imbalance). Default to OFF in ensemble mode. */ - if(use_seq_weights < 0.0f){ - use_seq_weights = 0.0f; - } - - /* Essential input check + detect alphabet */ - RUN(kalign_essential_input_check(msa, 0)); - - numseq = msa->numseq; - - DECLARE_TIMER(t_ensemble); - if(!msa->quiet){ - LOG_MSG("Ensemble alignment with %d runs", n_runs); - } - START_TIMER(t_ensemble); - - /* Resolve default gap penalties using aln_param_init. - We need to detect biotype first. */ - if(msa->biotype == ALN_BIOTYPE_UNDEF){ - RUN(detect_alphabet(msa)); - } - - /* Use aln_param_init to resolve defaults */ - RUN(aln_param_init(&ap, msa->biotype, n_threads, type, gpo, gpe, tgpe)); - base_gpo = ap->gpo; - base_gpe = ap->gpe; - base_tgpe = ap->tgpe; - aln_param_free(ap); - ap = NULL; - - /* Allocate POAR table and array to store completed alignments */ - RUN(poar_table_alloc(&poar, numseq)); - MMALLOC(alignments, sizeof(struct msa*) * n_runs); - for(k = 0; k < n_runs; k++){ - alignments[k] = NULL; - } - - /* Run N alignments, extract POARs, and keep each alignment */ - for(k = 0; k < n_runs; k++){ - float run_gpo, run_gpe, run_tgpe, run_noise; - uint64_t run_seed; - - resolve_run_params(base_gpo, base_gpe, base_tgpe, k, seed, - &run_gpo, &run_gpe, &run_tgpe, - &run_seed, &run_noise); - - /* Deep-copy MSA */ - copy = NULL; - RUN(msa_cpy(©, msa)); - copy->quiet = 1; - - if(!msa->quiet){ - LOG_MSG(" Run %d/%d (gpo=%.1f gpe=%.1f tgpe=%.1f noise=%.2f)", - k + 1, n_runs, run_gpo, run_gpe, run_tgpe, run_noise); - } - - /* Run alignment */ - if(realign > 0){ - RUN(kalign_run_realign(copy, n_threads, type, - run_gpo, run_gpe, run_tgpe, - refine, 0, - dist_scale, vsm_amax, - realign, use_seq_weights, - consistency_anchors, consistency_weight)); - }else{ - RUN(kalign_run_seeded(copy, n_threads, type, - run_gpo, run_gpe, run_tgpe, - refine, 0, - run_seed, run_noise, - dist_scale, vsm_amax, - use_seq_weights, - consistency_anchors, consistency_weight)); - } - - /* Extract POARs from the finalized alignment */ - char** aln_seqs = NULL; - MMALLOC(aln_seqs, sizeof(char*) * numseq); - for(int i = 0; i < numseq; i++){ - aln_seqs[i] = copy->sequences[i]->seq; - } - - RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); - RUN(extract_poars(poar, pm, k)); - - pos_matrix_free(pm); - pm = NULL; - MFREE(aln_seqs); - - /* Keep this alignment for scoring later */ - alignments[k] = copy; - copy = NULL; - } - - /* Score all alignments and select the best */ - RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, - &scores, &best_k)); - - if(!msa->quiet){ - LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); - } - - /* Save POAR table if requested */ - if(save_poar_path != NULL){ - RUN(poar_table_write(poar, save_poar_path)); - if(!msa->quiet){ - LOG_MSG(" Saved POAR table to %s", save_poar_path); - } - } - - /* When min_support > 0: explicit consensus threshold, skip selection. - When min_support == 0: auto behavior (selection vs consensus). */ - if(min_support > 0){ - /* Explicit consensus: force consensus path */ - RUN(build_consensus_from_poar(poar, msa, numseq, min_support, - &consensus_msa)); - use_consensus = 1; - if(!msa->quiet){ - LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); - } - }else{ - /* Try consensus approach: build a new alignment from POAR table. - This can combine correct pairs from multiple runs, potentially - outperforming any single run. */ - double consensus_score = 0.0; - int min_sup = (n_runs + 2) / 3; - if(min_sup < 2) min_sup = 2; - - RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, - &consensus_msa)); +/* kalign_ensemble and kalign_ensemble_custom removed — + use kalign_align_full with per-run configs instead. */ - /* Score consensus against POAR table */ - RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, - &consensus_score)); +/* ---- Parallel ensemble run support ---- */ +#ifdef USE_THREADPOOL +#include "threadpool/threadpool.h" - if(!msa->quiet){ - LOG_MSG(" Consensus score: %.1f (selection: %.1f)", - consensus_score, scores[best_k]); - } - - if(consensus_score > scores[best_k]){ - use_consensus = 1; - if(!msa->quiet){ - LOG_MSG(" Using consensus alignment"); - } - }else{ - kalign_free_msa(consensus_msa); - consensus_msa = NULL; - if(!msa->quiet){ - LOG_MSG(" Keeping selection winner"); - } - } - } - - /* Post-selection refinement: only when using selection (not consensus), - re-run the winner with REFINE_CONFIDENT and keep if it scores higher. */ - if(!use_consensus){ - float ref_gpo, ref_gpe, ref_tgpe, ref_noise; - uint64_t ref_seed; - - resolve_run_params(base_gpo, base_gpe, base_tgpe, best_k, seed, - &ref_gpo, &ref_gpe, &ref_tgpe, - &ref_seed, &ref_noise); - - copy = NULL; - RUN(msa_cpy(©, msa)); - copy->quiet = 1; - - if(!msa->quiet){ - LOG_MSG(" Refining run %d...", best_k + 1); - } - - RUN(kalign_run_seeded(copy, n_threads, type, - ref_gpo, ref_gpe, ref_tgpe, - KALIGN_REFINE_CONFIDENT, 0, - ref_seed, ref_noise, - dist_scale, vsm_amax, - use_seq_weights, - consistency_anchors, consistency_weight)); - - /* Score the refined alignment against the same POAR table */ - double refined_score = 0.0; - RUN(score_single_msa(copy, poar, numseq, n_runs, - &refined_score)); - - if(!msa->quiet){ - LOG_MSG(" Refined score: %.1f (was %.1f)", - refined_score, scores[best_k]); - } - - if(refined_score > scores[best_k]){ - kalign_free_msa(alignments[best_k]); - alignments[best_k] = copy; - copy = NULL; - if(!msa->quiet){ - LOG_MSG(" Using refined alignment"); - } - }else{ - kalign_free_msa(copy); - copy = NULL; - if(!msa->quiet){ - LOG_MSG(" Keeping original alignment"); - } - } - } - - MFREE(scores); - scores = NULL; - - /* Copy the winning alignment back into the original MSA */ - if(use_consensus){ - RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); - kalign_free_msa(consensus_msa); - consensus_msa = NULL; - }else{ - RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); - } - - /* Compute per-residue and per-column confidence from POAR table */ - RUN(compute_residue_confidence(poar, msa)); - - /* Sort back to original rank order */ - RUN(msa_sort_rank(msa)); - - STOP_TIMER(t_ensemble); - if(!msa->quiet){ - GET_TIMING(t_ensemble); - } - DESTROY_TIMER(t_ensemble); - - /* Free all alignments */ - for(k = 0; k < n_runs; k++){ - if(alignments[k]) kalign_free_msa(alignments[k]); - } - MFREE(alignments); - poar_table_free(poar); - return OK; -ERROR: - if(copy) kalign_free_msa(copy); - if(consensus_msa) kalign_free_msa(consensus_msa); - if(pm) pos_matrix_free(pm); - if(alignments){ - for(k = 0; k < n_runs; k++){ - if(alignments[k]) kalign_free_msa(alignments[k]); - } - MFREE(alignments); - } - poar_table_free(poar); - if(scores) MFREE(scores); - if(ap) aln_param_free(ap); - return FAIL; -} +struct ensemble_run_arg { + struct msa* copy; + const struct kalign_run_config* cfg; + int n_threads; + int error; +}; -/* ======================================================================== */ -/* kalign_ensemble_custom: like kalign_ensemble but with per-run parameters. - * - * Instead of a hardcoded scale-factor table, each run gets its own - * gap penalties, matrix type, and tree noise via arrays. - * - * run_gpo[n_runs], run_gpe[n_runs], run_tgpe[n_runs]: per-run gap penalties - * run_types[n_runs]: per-run matrix type (KALIGN_TYPE_PROTEIN, _PFASUM43, etc.) - * Pass NULL to use the same 'type' for all runs. - * run_noise[n_runs]: per-run tree noise sigma - * - * All other parameters (vsm_amax, realign, consistency, etc.) are shared - * across runs — they affect *how* each alignment is computed, not *what* - * gap/matrix parameters it uses. - */ -int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, - int n_runs, - const float* run_gpo, - const float* run_gpe, - const float* run_tgpe, - const int* run_types, - const float* run_noise, - uint64_t seed, int min_support, - int refine, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight) +static void ensemble_run_task_fn(void* arg) { - struct msa* copy = NULL; - struct msa* consensus_msa = NULL; - struct msa** alignments = NULL; - struct poar_table* poar = NULL; - struct pos_matrix* pm = NULL; - double* scores = NULL; - int numseq; - int k; - int best_k = 0; - int use_consensus = 0; - - ASSERT(msa != NULL, "No MSA"); - ASSERT(n_runs >= 1, "n_runs must be >= 1"); - ASSERT(run_gpo != NULL, "run_gpo is NULL"); - ASSERT(run_gpe != NULL, "run_gpe is NULL"); - ASSERT(run_tgpe != NULL, "run_tgpe is NULL"); - ASSERT(run_noise != NULL, "run_noise is NULL"); - - if(use_seq_weights < 0.0f){ - use_seq_weights = 0.0f; - } - - RUN(kalign_essential_input_check(msa, 0)); - - numseq = msa->numseq; - - DECLARE_TIMER(t_ensemble); - if(!msa->quiet){ - LOG_MSG("Custom ensemble alignment with %d runs", n_runs); + struct ensemble_run_arg* ra = (struct ensemble_run_arg*)arg; + if(kalign_single_run(ra->copy, ra->cfg, ra->n_threads) != 0){ + ra->error = 1; } - START_TIMER(t_ensemble); - - if(msa->biotype == ALN_BIOTYPE_UNDEF){ - RUN(detect_alphabet(msa)); - } - - RUN(poar_table_alloc(&poar, numseq)); - MMALLOC(alignments, sizeof(struct msa*) * n_runs); - for(k = 0; k < n_runs; k++){ - alignments[k] = NULL; - } - - for(k = 0; k < n_runs; k++){ - int run_type = (run_types != NULL) ? run_types[k] : type; - uint64_t run_seed = seed + (uint64_t)k; - - copy = NULL; - RUN(msa_cpy(©, msa)); - copy->quiet = 1; - - if(!msa->quiet){ - LOG_MSG(" Run %d/%d (gpo=%.2f gpe=%.2f tgpe=%.2f noise=%.2f type=%d)", - k + 1, n_runs, run_gpo[k], run_gpe[k], run_tgpe[k], - run_noise[k], run_type); - } - - if(realign > 0){ - RUN(kalign_run_realign(copy, n_threads, run_type, - run_gpo[k], run_gpe[k], run_tgpe[k], - refine, 0, - 0.0f, vsm_amax, - realign, use_seq_weights, - consistency_anchors, consistency_weight)); - }else{ - RUN(kalign_run_seeded(copy, n_threads, run_type, - run_gpo[k], run_gpe[k], run_tgpe[k], - refine, 0, - run_seed, run_noise[k], - 0.0f, vsm_amax, - use_seq_weights, - consistency_anchors, consistency_weight)); - } - - char** aln_seqs = NULL; - MMALLOC(aln_seqs, sizeof(char*) * numseq); - for(int i = 0; i < numseq; i++){ - aln_seqs[i] = copy->sequences[i]->seq; - } - - RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); - RUN(extract_poars(poar, pm, k)); - - pos_matrix_free(pm); - pm = NULL; - MFREE(aln_seqs); - - alignments[k] = copy; - copy = NULL; - } - - RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, - &scores, &best_k)); - - if(!msa->quiet){ - LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); - } - - if(min_support > 0){ - RUN(build_consensus_from_poar(poar, msa, numseq, min_support, - &consensus_msa)); - use_consensus = 1; - if(!msa->quiet){ - LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); - } - }else{ - double consensus_score = 0.0; - int min_sup = (n_runs + 2) / 3; - if(min_sup < 2) min_sup = 2; - - RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, - &consensus_msa)); - - RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, - &consensus_score)); - - if(!msa->quiet){ - LOG_MSG(" Consensus score: %.1f (selection: %.1f)", - consensus_score, scores[best_k]); - } - - if(consensus_score > scores[best_k]){ - use_consensus = 1; - if(!msa->quiet){ - LOG_MSG(" Using consensus alignment"); - } - }else{ - kalign_free_msa(consensus_msa); - consensus_msa = NULL; - if(!msa->quiet){ - LOG_MSG(" Keeping selection winner"); - } - } - } - - /* Post-selection refinement */ - if(!use_consensus){ - int ref_type = (run_types != NULL) ? run_types[best_k] : type; - uint64_t ref_seed = seed + (uint64_t)best_k; - - copy = NULL; - RUN(msa_cpy(©, msa)); - copy->quiet = 1; - - if(!msa->quiet){ - LOG_MSG(" Refining run %d...", best_k + 1); - } - - RUN(kalign_run_seeded(copy, n_threads, ref_type, - run_gpo[best_k], run_gpe[best_k], run_tgpe[best_k], - KALIGN_REFINE_CONFIDENT, 0, - ref_seed, run_noise[best_k], - 0.0f, vsm_amax, - use_seq_weights, - consistency_anchors, consistency_weight)); - - double refined_score = 0.0; - RUN(score_single_msa(copy, poar, numseq, n_runs, - &refined_score)); - - if(!msa->quiet){ - LOG_MSG(" Refined score: %.1f (was %.1f)", - refined_score, scores[best_k]); - } - - if(refined_score > scores[best_k]){ - kalign_free_msa(alignments[best_k]); - alignments[best_k] = copy; - copy = NULL; - if(!msa->quiet){ - LOG_MSG(" Using refined alignment"); - } - }else{ - kalign_free_msa(copy); - copy = NULL; - if(!msa->quiet){ - LOG_MSG(" Keeping original alignment"); - } - } - } - - MFREE(scores); - scores = NULL; - - if(use_consensus){ - RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); - kalign_free_msa(consensus_msa); - consensus_msa = NULL; - }else{ - RUN(copy_alignment_to_msa(msa, alignments[best_k], numseq)); - } - - RUN(compute_residue_confidence(poar, msa)); - RUN(msa_sort_rank(msa)); - - STOP_TIMER(t_ensemble); - if(!msa->quiet){ - GET_TIMING(t_ensemble); - } - DESTROY_TIMER(t_ensemble); - - for(k = 0; k < n_runs; k++){ - if(alignments[k]) kalign_free_msa(alignments[k]); - } - MFREE(alignments); - poar_table_free(poar); - return OK; -ERROR: - if(copy) kalign_free_msa(copy); - if(consensus_msa) kalign_free_msa(consensus_msa); - if(pm) pos_matrix_free(pm); - if(alignments){ - for(k = 0; k < n_runs; k++){ - if(alignments[k]) kalign_free_msa(alignments[k]); - } - MFREE(alignments); - } - poar_table_free(poar); - if(scores) MFREE(scores); - return FAIL; } +#endif /* ======================================================================== */ + /* kalign_generate_ensemble_runs: expand base config into N diversified runs. * * IMPORTANT: base.gpo/gpe/tgpe must be resolved (non-sentinel) values. @@ -836,67 +327,143 @@ int kalign_ensemble_from_configs(struct msa* msa, alignments[k] = NULL; } - /* Run N alignments */ - for(k = 0; k < n_runs; k++){ - copy = NULL; - RUN(msa_cpy(©, msa)); - copy->quiet = 1; + /* Phase timing instrumentation */ + DECLARE_TIMER(t_phase); - if(!msa->quiet){ - LOG_MSG(" Run %d/%d (gpo=%.1f gpe=%.1f tgpe=%.1f noise=%.2f)", - k + 1, n_runs, - runs[k].gpo, runs[k].gpe, runs[k].tgpe, - runs[k].tree_noise); + /* Run N alignments concurrently — all runs share the one global + threadpool, giving the pool N× more tasks to keep workers busy. + POAR extraction is sequential (sorted insert not thread-safe). */ + START_TIMER(t_phase); +#ifdef USE_THREADPOOL + if(msa->pool != NULL && n_runs > 1){ + struct ensemble_run_arg* run_args = NULL; + MMALLOC(run_args, sizeof(struct ensemble_run_arg) * n_runs); + + for(k = 0; k < n_runs; k++){ + run_args[k].copy = NULL; + RUN(msa_cpy(&run_args[k].copy, msa)); + run_args[k].copy->quiet = 1; + run_args[k].copy->pool = msa->pool; + run_args[k].cfg = &runs[k]; + run_args[k].n_threads = n_threads; + run_args[k].error = 0; + + if(!msa->quiet){ + LOG_MSG(" Run %d/%d (gpo=%.1f gpe=%.1f tgpe=%.1f noise=%.2f)", + k + 1, n_runs, + runs[k].gpo, runs[k].gpe, runs[k].tgpe, + runs[k].tree_noise); + } } - if(runs[k].realign > 0){ - RUN(kalign_run_realign(copy, n_threads, runs[k].matrix, - runs[k].gpo, runs[k].gpe, runs[k].tgpe, - runs[k].refine, 0, - runs[k].dist_scale, runs[k].vsm_amax, - runs[k].realign, runs[k].seq_weights, - runs[k].consistency_anchors, - runs[k].consistency_weight)); - }else{ - RUN(kalign_run_seeded(copy, n_threads, runs[k].matrix, - runs[k].gpo, runs[k].gpe, runs[k].tgpe, - runs[k].refine, 0, - runs[k].tree_seed, runs[k].tree_noise, - runs[k].dist_scale, runs[k].vsm_amax, - runs[k].seq_weights, - runs[k].consistency_anchors, - runs[k].consistency_weight)); + /* Fork all runs into the shared pool */ + { + tp_group_t *g = tp_group_create(msa->pool); + for(k = 0; k < n_runs; k++){ + tp_group_submit(g, ensemble_run_task_fn, &run_args[k]); + } + tp_group_wait(g); + tp_group_destroy(g); } - /* Extract POARs from the finalized alignment */ - char** aln_seqs = NULL; - MMALLOC(aln_seqs, sizeof(char*) * numseq); - for(int i = 0; i < numseq; i++){ - aln_seqs[i] = copy->sequences[i]->seq; + /* Check for errors and collect results */ + for(k = 0; k < n_runs; k++){ + if(run_args[k].error){ + for(int j = 0; j < n_runs; j++){ + if(run_args[j].copy) kalign_free_msa(run_args[j].copy); + } + MFREE(run_args); + ERROR_MSG("Ensemble run %d failed", k + 1); + } + alignments[k] = run_args[k].copy; + run_args[k].copy = NULL; } + MFREE(run_args); - RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); - RUN(extract_poars(poar, pm, k)); + /* Extract POARs sequentially */ + for(k = 0; k < n_runs; k++){ + char** aln_seqs = NULL; + MMALLOC(aln_seqs, sizeof(char*) * numseq); + for(int i = 0; i < numseq; i++){ + aln_seqs[i] = alignments[k]->sequences[i]->seq; + } + RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, alignments[k]->alnlen)); + { +#ifdef USE_THREADPOOL + int _ep_ret = extract_poars(poar, pm, k, msa->pool); +#else + int _ep_ret = extract_poars(poar, pm, k); +#endif + if(_ep_ret != OK) goto ERROR; + } + pos_matrix_free(pm); + pm = NULL; + MFREE(aln_seqs); + } + }else +#endif + { + /* Sequential fallback (no threadpool or single run) */ + for(k = 0; k < n_runs; k++){ + copy = NULL; + RUN(msa_cpy(©, msa)); + copy->quiet = 1; +#ifdef USE_THREADPOOL + copy->pool = msa->pool; +#endif + if(!msa->quiet){ + LOG_MSG(" Run %d/%d (gpo=%.1f gpe=%.1f tgpe=%.1f noise=%.2f)", + k + 1, n_runs, + runs[k].gpo, runs[k].gpe, runs[k].tgpe, + runs[k].tree_noise); + } + RUN(kalign_single_run(copy, &runs[k], n_threads)); - pos_matrix_free(pm); - pm = NULL; - MFREE(aln_seqs); + char** aln_seqs = NULL; + MMALLOC(aln_seqs, sizeof(char*) * numseq); + for(int i = 0; i < numseq; i++){ + aln_seqs[i] = copy->sequences[i]->seq; + } + RUN(pos_matrix_from_msa(&pm, aln_seqs, numseq, copy->alnlen)); + { +#ifdef USE_THREADPOOL + int _ep_ret = extract_poars(poar, pm, k, msa->pool); +#else + int _ep_ret = extract_poars(poar, pm, k); +#endif + if(_ep_ret != OK) goto ERROR; + } + pos_matrix_free(pm); + pm = NULL; + MFREE(aln_seqs); - alignments[k] = copy; - copy = NULL; + alignments[k] = copy; + copy = NULL; + } } + STOP_TIMER(t_phase); + if(!msa->quiet){ LOG_MSG(" [time] alignment runs + POAR extraction: "); GET_TIMING(t_phase); } + /* Score all alignments and select the best */ + START_TIMER(t_phase); + #ifdef USE_THREADPOOL + RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, + &scores, &best_k, msa->pool)); +#else RUN(score_alignments(alignments, poar, numseq, n_runs, msa->quiet, &scores, &best_k)); +#endif if(!msa->quiet){ LOG_MSG(" Selected run %d (score=%.1f)", best_k + 1, scores[best_k]); } - /* POAR save removed from ensemble_config — debug feature */ + STOP_TIMER(t_phase); + if(!msa->quiet){ LOG_MSG(" [time] scoring: "); GET_TIMING(t_phase); } /* Determine merge strategy */ + START_TIMER(t_phase); int min_support = (ens != NULL) ? ens->min_support : 0; int use_consistency_merge = (ens != NULL) ? ens->consistency_merge : 0; @@ -927,15 +494,19 @@ int kalign_ensemble_from_configs(struct msa* msa, /* Run a fresh progressive alignment with best_k's params. No additional anchor consistency or realign — the POAR consistency signal is the main guide. */ - RUN(kalign_run_seeded(copy, n_threads, runs[best_k].matrix, - runs[best_k].gpo, runs[best_k].gpe, - runs[best_k].tgpe, - KALIGN_REFINE_NONE, 0, - 0, 0.0f, /* deterministic tree */ - runs[best_k].dist_scale, - runs[best_k].vsm_amax, - 0.0f, /* no seq_weights in ensemble */ - 0, 0.0f /* no anchor consistency */)); + { + struct kalign_run_config cm_cfg = runs[best_k]; + cm_cfg.refine = KALIGN_REFINE_NONE; + cm_cfg.tree_seed = 0; + cm_cfg.tree_noise = 0.0f; + cm_cfg.seq_weights = 0.0f; + cm_cfg.consistency_anchors = 0; + cm_cfg.realign = 0; +#ifdef USE_THREADPOOL + copy->pool = msa->pool; +#endif + RUN(kalign_single_run(copy, &cm_cfg, n_threads)); + } /* Clear the non-owning pointer before freeing the copy */ copy->poar_consistency = NULL; @@ -948,8 +519,13 @@ int kalign_ensemble_from_configs(struct msa* msa, /* ---- POAR consensus / selection path (existing) ---- */ if(min_support > 0){ + #ifdef USE_THREADPOOL + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa, msa->pool)); + #else RUN(build_consensus_from_poar(poar, msa, numseq, min_support, &consensus_msa)); + #endif use_consensus = 1; if(!msa->quiet){ LOG_MSG(" Using consensus alignment (min_support=%d)", min_support); @@ -959,11 +535,21 @@ int kalign_ensemble_from_configs(struct msa* msa, int min_sup = (n_runs + 2) / 3; if(min_sup < 2) min_sup = 2; + #ifdef USE_THREADPOOL + RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, + &consensus_msa, msa->pool)); + #else RUN(build_consensus_from_poar(poar, msa, numseq, min_sup, &consensus_msa)); + #endif + #ifdef USE_THREADPOOL + RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, + &consensus_score, msa->pool)); + #else RUN(score_single_msa(consensus_msa, poar, numseq, n_runs, &consensus_score)); + #endif if(!msa->quiet){ LOG_MSG(" Consensus score: %.1f (selection: %.1f)", @@ -986,27 +572,30 @@ int kalign_ensemble_from_configs(struct msa* msa, /* Post-selection refinement: re-run the winner with REFINE_CONFIDENT */ if(!use_consensus){ + struct kalign_run_config ref_cfg = runs[best_k]; + ref_cfg.refine = KALIGN_REFINE_CONFIDENT; + copy = NULL; RUN(msa_cpy(©, msa)); copy->quiet = 1; +#ifdef USE_THREADPOOL + copy->pool = msa->pool; +#endif if(!msa->quiet){ LOG_MSG(" Refining run %d...", best_k + 1); } - RUN(kalign_run_seeded(copy, n_threads, runs[best_k].matrix, - runs[best_k].gpo, runs[best_k].gpe, - runs[best_k].tgpe, - KALIGN_REFINE_CONFIDENT, 0, - runs[best_k].tree_seed, runs[best_k].tree_noise, - runs[best_k].dist_scale, runs[best_k].vsm_amax, - runs[best_k].seq_weights, - runs[best_k].consistency_anchors, - runs[best_k].consistency_weight)); + RUN(kalign_single_run(copy, &ref_cfg, n_threads)); double refined_score = 0.0; + #ifdef USE_THREADPOOL + RUN(score_single_msa(copy, poar, numseq, n_runs, + &refined_score, msa->pool)); + #else RUN(score_single_msa(copy, poar, numseq, n_runs, &refined_score)); + #endif if(!msa->quiet){ LOG_MSG(" Refined score: %.1f (was %.1f)", @@ -1046,7 +635,18 @@ int kalign_ensemble_from_configs(struct msa* msa, scores = NULL; } + STOP_TIMER(t_phase); + if(!msa->quiet){ LOG_MSG(" [time] consensus/selection: "); GET_TIMING(t_phase); } + + START_TIMER(t_phase); + #ifdef USE_THREADPOOL + RUN(compute_residue_confidence(poar, msa, msa->pool)); +#else RUN(compute_residue_confidence(poar, msa)); +#endif + STOP_TIMER(t_phase); + if(!msa->quiet){ LOG_MSG(" [time] confidence: "); GET_TIMING(t_phase); } + RUN(msa_sort_rank(msa)); STOP_TIMER(t_ensemble); @@ -1054,6 +654,7 @@ int kalign_ensemble_from_configs(struct msa* msa, GET_TIMING(t_ensemble); } DESTROY_TIMER(t_ensemble); + DESTROY_TIMER(t_phase); for(k = 0; k < n_runs; k++){ if(alignments[k]) kalign_free_msa(alignments[k]); @@ -1102,8 +703,18 @@ int kalign_consensus_from_poar(struct msa* msa, } /* Build consensus at given min_support threshold */ - RUN(build_consensus_from_poar(poar, msa, numseq, min_support, - &consensus_msa)); + #ifdef USE_THREADPOOL + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa, msa->pool)); +#else + #ifdef USE_THREADPOOL + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa, msa->pool)); + #else + RUN(build_consensus_from_poar(poar, msa, numseq, min_support, + &consensus_msa)); + #endif +#endif /* Copy consensus alignment back into original MSA */ RUN(copy_alignment_to_msa(msa, consensus_msa, numseq)); @@ -1111,7 +722,11 @@ int kalign_consensus_from_poar(struct msa* msa, consensus_msa = NULL; /* Compute per-residue and per-column confidence */ + #ifdef USE_THREADPOOL + RUN(compute_residue_confidence(poar, msa, msa->pool)); +#else RUN(compute_residue_confidence(poar, msa)); +#endif RUN(msa_sort_rank(msa)); diff --git a/lib/src/ensemble.h b/lib/src/ensemble.h index 62741de..fa5a94a 100644 --- a/lib/src/ensemble.h +++ b/lib/src/ensemble.h @@ -16,26 +16,6 @@ struct msa; -EXTERN int kalign_ensemble(struct msa* msa, int n_threads, int type, - int n_runs, float gpo, float gpe, float tgpe, - uint64_t seed, int min_support, - const char* save_poar_path, - int refine, float dist_scale, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight); - -EXTERN int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, - int n_runs, - const float* run_gpo, - const float* run_gpe, - const float* run_tgpe, - const int* run_types, - const float* run_noise, - uint64_t seed, int min_support, - int refine, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight); - EXTERN int kalign_ensemble_from_configs(struct msa* msa, const struct kalign_run_config* runs, int n_runs, diff --git a/lib/src/poar.c b/lib/src/poar.c index c9cbf6a..3231375 100644 --- a/lib/src/poar.c +++ b/lib/src/poar.c @@ -4,6 +4,10 @@ #include #include +#ifdef USE_THREADPOOL +#include "threadpool/threadpool.h" +#endif + #define POAR_IMPORT #include "poar.h" @@ -168,7 +172,46 @@ void pos_matrix_free(struct pos_matrix* pm) } } -int extract_poars(struct poar_table* table, struct pos_matrix* pm, int aln_idx) +#ifdef USE_THREADPOOL +struct extract_poar_ctx { + struct poar_table* table; + struct pos_matrix* pm; + int numseq; + int alnlen; + int aln_idx; + int error; +}; + +static void extract_poar_chunk(int start, int end, void* arg) +{ + struct extract_poar_ctx* c = (struct extract_poar_ctx*)arg; + int j, col; + for(int i = start; i < end; i++){ + for(j = i + 1; j < c->numseq; j++){ + int pidx = pair_index(i, j, c->numseq); + struct poar_pair* pp = c->table->pairs[pidx]; + for(col = 0; col < c->alnlen; col++){ + int ri = c->pm->col_to_res[i][col]; + int rj = c->pm->col_to_res[j][col]; + if(ri >= 0 && rj >= 0){ + uint32_t key = pack_key(ri, rj); + if(poar_pair_insert(pp, key, c->aln_idx) != OK){ + c->error = 1; + return; + } + } + } + } + } +} +#endif + +int extract_poars(struct poar_table* table, struct pos_matrix* pm, + int aln_idx +#ifdef USE_THREADPOOL + , threadpool_t* pool +#endif + ) { int i, j, col; int numseq = pm->numseq; @@ -176,17 +219,30 @@ int extract_poars(struct poar_table* table, struct pos_matrix* pm, int aln_idx) ASSERT(aln_idx < 32, "Maximum 32 alignments supported in ensemble"); - for(i = 0; i < numseq - 1; i++){ - for(j = i + 1; j < numseq; j++){ - int pidx = pair_index(i, j, numseq); - struct poar_pair* pp = table->pairs[pidx]; - - for(col = 0; col < alnlen; col++){ - int ri = pm->col_to_res[i][col]; - int rj = pm->col_to_res[j][col]; - if(ri >= 0 && rj >= 0){ - uint32_t key = pack_key(ri, rj); - RUN(poar_pair_insert(pp, key, aln_idx)); +#ifdef USE_THREADPOOL + if(pool != NULL && numseq > 16){ + struct extract_poar_ctx ctx = { + table, pm, numseq, alnlen, aln_idx, 0 + }; + tp_parallel_for(pool, 0, numseq - 1, + extract_poar_chunk, &ctx); + if(ctx.error){ + ERROR_MSG("Parallel POAR extraction failed"); + } + }else +#endif + { + for(i = 0; i < numseq - 1; i++){ + for(j = i + 1; j < numseq; j++){ + int pidx = pair_index(i, j, numseq); + struct poar_pair* pp = table->pairs[pidx]; + for(col = 0; col < alnlen; col++){ + int ri = pm->col_to_res[i][col]; + int rj = pm->col_to_res[j][col]; + if(ri >= 0 && rj >= 0){ + uint32_t key = pack_key(ri, rj); + RUN(poar_pair_insert(pp, key, aln_idx)); + } } } } diff --git a/lib/src/poar.h b/lib/src/poar.h index 7921814..9d652ea 100644 --- a/lib/src/poar.h +++ b/lib/src/poar.h @@ -47,7 +47,14 @@ EXTERN void poar_table_free(struct poar_table* table); EXTERN int pos_matrix_from_msa(struct pos_matrix** pm, char** seqs, int numseq, int alnlen); EXTERN void pos_matrix_free(struct pos_matrix* pm); -EXTERN int extract_poars(struct poar_table* table, struct pos_matrix* pm, int aln_idx); +#ifdef USE_THREADPOOL +typedef struct threadpool threadpool_t; +EXTERN int extract_poars(struct poar_table* table, struct pos_matrix* pm, + int aln_idx, threadpool_t* pool); +#else +EXTERN int extract_poars(struct poar_table* table, struct pos_matrix* pm, + int aln_idx); +#endif EXTERN int poar_table_write(struct poar_table* table, const char* path); EXTERN int poar_table_read(struct poar_table** table, const char* path); diff --git a/scripts/bench_accuracy.py b/scripts/bench_accuracy.py new file mode 100644 index 0000000..ae035e8 --- /dev/null +++ b/scripts/bench_accuracy.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Alignment accuracy benchmark — reuses scoring from the manuscript repo. + +Discovers BAliBASE cases, runs kalign fast/default/recall/accurate, +scores with XML core block masks (same as manuscript pipeline). + +Usage: + uv run python scripts/bench_accuracy.py + uv run python scripts/bench_accuracy.py --modes fast,accurate +""" +import argparse +import json +import logging +import sys +import tempfile +import xml.etree.ElementTree as ET +from collections import defaultdict +from pathlib import Path + +import kalign + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +BB_ROOT = Path(__file__).parent.parent / "benchmarks" / "data" / "downloads" / "bb3_release" + + +def parse_balibase_xml(xml_path): + """Parse BAliBASE XML to get core block column mask (from manuscript scoring.py).""" + tree = ET.parse(xml_path) + root = tree.getroot() + colsco = root.find(".//column-score/colsco-data") + if colsco is None or colsco.text is None: + return None + values = [int(v) for v in colsco.text.split()] + return [1 if v == 1 else 0 for v in values] + + +def discover_cases(): + """Find all BAliBASE cases (excluding BBS supplement).""" + cases = [] + for rv_dir in sorted(BB_ROOT.iterdir()): + if not rv_dir.is_dir() or not rv_dir.name.startswith("RV"): + continue + for tfa in sorted(rv_dir.glob("*.tfa")): + if tfa.stem.startswith("BBS"): + continue + msf = tfa.with_suffix(".msf") + xml = tfa.with_suffix(".xml") + if msf.exists(): + cases.append({ + "family": tfa.stem, + "category": rv_dir.name, + "unaligned": str(tfa), + "reference": str(msf), + "xml": str(xml) if xml.exists() else None, + }) + return cases + + +def score_case(ref_path, test_path, xml_path): + """Score one alignment using XML core blocks if available.""" + if xml_path and Path(xml_path).exists(): + mask = parse_balibase_xml(xml_path) + if mask: + return kalign.compare_detailed(ref_path, test_path, column_mask=mask) + return kalign.compare_detailed(ref_path, test_path, max_gap_frac=0.2) + + +def main(): + parser = argparse.ArgumentParser(description="BAliBASE accuracy benchmark") + parser.add_argument("--modes", default="fast,default,recall,accurate") + parser.add_argument("--threads", type=int, default=1) + parser.add_argument("--output", help="Save results JSON") + args = parser.parse_args() + + if not BB_ROOT.exists(): + logger.error("BAliBASE not found at %s", BB_ROOT) + return 1 + + modes = args.modes.split(",") + cases = discover_cases() + logger.info("%d BAliBASE cases, modes: %s", len(cases), modes) + + results = [] + for mode in modes: + with tempfile.TemporaryDirectory() as tmpdir: + for i, case in enumerate(cases): + out = str(Path(tmpdir) / f"{case['family']}.fa") + kalign.align_file_to_file( + case["unaligned"], out, + mode=mode, n_threads=args.threads, + ) + score = score_case(case["reference"], out, case["xml"]) + results.append({ + "family": case["family"], + "category": case["category"], + "mode": mode, + **score, + }) + if (i + 1) % 50 == 0: + logger.info(" %s: %d/%d", mode, i + 1, len(cases)) + logger.info(" %s: done", mode) + + # Aggregate and print + print() + print(f"{'Mode':12s} {'Recall':>8s} {'Prec':>8s} {'F1':>8s} {'TC':>8s} (n)") + print("=" * 52) + for mode in modes: + mc = [r for r in results if r["mode"] == mode] + n = len(mc) + r = sum(c["recall"] for c in mc) / n + p = sum(c["precision"] for c in mc) / n + f = sum(c["f1"] for c in mc) / n + t = sum(c["tc"] for c in mc) / n + print(f"{mode:12s} {r:8.3f} {p:8.3f} {f:8.3f} {t:8.3f} ({n})") + + if args.output: + with open(args.output, "w") as f: + json.dump(results, f, indent=2) + logger.info("Saved to %s", args.output) + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/scripts/verify_balibase.py b/scripts/verify_balibase.py new file mode 100644 index 0000000..305e325 --- /dev/null +++ b/scripts/verify_balibase.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""BAliBASE verification: generate checksums or compare against baseline. + +Usage: + # Generate baseline checksums (run with CURRENT code, 1 thread) + uv run python scripts/verify_balibase.py baseline --output baseline_checksums.json + + # Compare against baseline (run AFTER code change + rebuild) + uv run python scripts/verify_balibase.py compare --baseline baseline_checksums.json + + # Timing benchmark (20 largest cases, multiple thread counts) + uv run python scripts/verify_balibase.py timing --threads 1,4,8,16 + + # Quick test (10 cases only) + uv run python scripts/verify_balibase.py baseline --output baseline.json --max-cases 10 +""" + +import argparse +import hashlib +import json +import os +import sys +import tempfile +import time +from pathlib import Path + +import kalign + + +BB_ROOT = Path(__file__).parent.parent / "benchmarks" / "data" / "downloads" / "bb3_release" +MODES = ["fast", "default", "accurate"] + + +def get_cases(max_cases=0): + """Discover all BAliBASE .tfa files, sorted by size (largest first for timing).""" + cases = [] + for rv_dir in sorted(BB_ROOT.iterdir()): + if not rv_dir.is_dir() or not rv_dir.name.startswith("RV"): + continue + for tfa in sorted(rv_dir.glob("*.tfa")): + msf = tfa.with_suffix(".msf") + if msf.exists(): + cases.append({ + "name": tfa.stem, + "category": rv_dir.name, + "unaligned": str(tfa), + "reference": str(msf), + "size": tfa.stat().st_size, + }) + # Sort by size descending (largest first — useful for timing) + cases.sort(key=lambda c: -c["size"]) + if max_cases > 0: + cases = cases[:max_cases] + return cases + + +def sha256_file(path): + """Compute SHA256 of a file.""" + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +def run_alignment(case, mode, n_threads, output_path): + """Run one alignment, return (sha256, elapsed_seconds).""" + t0 = time.perf_counter() + kalign.align_file_to_file( + case["unaligned"], output_path, + mode=mode, n_threads=n_threads, format="fasta", + ) + elapsed = time.perf_counter() - t0 + checksum = sha256_file(output_path) + return checksum, elapsed + + +def cmd_baseline(args): + """Generate baseline checksums for all cases × all modes.""" + cases = get_cases(args.max_cases) + modes = args.modes.split(",") if args.modes else MODES + n_threads = 1 # baseline always single-threaded for determinism + + print(f"Generating baseline: {len(cases)} cases × {len(modes)} modes, {n_threads} thread") + print(f"BAliBASE root: {BB_ROOT}") + + results = {} + total = len(cases) * len(modes) + done = 0 + + with tempfile.TemporaryDirectory() as tmpdir: + for case in cases: + for mode in modes: + out_path = os.path.join(tmpdir, f"{case['name']}_{mode}.fa") + checksum, elapsed = run_alignment(case, mode, n_threads, out_path) + + key = f"{case['name']}:{mode}" + results[key] = { + "checksum": checksum, + "time": round(elapsed, 3), + "category": case["category"], + } + + done += 1 + if args.verbose: + print(f" [{done}/{total}] {key}: {checksum[:12]}... ({elapsed:.2f}s)") + elif done % 50 == 0: + print(f" [{done}/{total}]...") + + # Save + output = { + "n_cases": len(cases), + "modes": modes, + "n_threads": n_threads, + "checksums": results, + } + with open(args.output, "w") as f: + json.dump(output, f, indent=2) + + print(f"\nBaseline saved to {args.output}: {len(results)} checksums") + + +def cmd_compare(args): + """Compare current code output against baseline checksums.""" + with open(args.baseline) as f: + baseline = json.load(f) + + cases_by_name = {c["name"]: c for c in get_cases()} + modes = baseline["modes"] + n_threads = args.threads # can test with >1 thread + + checksums = baseline["checksums"] + print(f"Comparing against baseline: {len(checksums)} checksums, {n_threads} threads") + + mismatches = [] + matches = 0 + + with tempfile.TemporaryDirectory() as tmpdir: + total = len(checksums) + done = 0 + + for key, bdata in checksums.items(): + name, mode = key.split(":") + if name not in cases_by_name: + print(f" SKIP {key}: case not found") + continue + + case = cases_by_name[name] + out_path = os.path.join(tmpdir, f"{name}_{mode}.fa") + checksum, elapsed = run_alignment(case, mode, n_threads, out_path) + + done += 1 + if checksum == bdata["checksum"]: + matches += 1 + if args.verbose: + print(f" [{done}/{total}] {key}: OK ({elapsed:.2f}s)") + else: + mismatches.append({ + "key": key, + "baseline": bdata["checksum"], + "current": checksum, + }) + print(f" [{done}/{total}] {key}: MISMATCH!") + print(f" baseline: {bdata['checksum'][:16]}...") + print(f" current: {checksum[:16]}...") + + if not args.verbose and done % 50 == 0: + print(f" [{done}/{total}] ({matches} match, {len(mismatches)} mismatch)...") + + print(f"\n{'='*60}") + print(f"Results: {matches} identical, {len(mismatches)} mismatched (of {done})") + + if mismatches: + print(f"\nMISMATCHES:") + for m in mismatches: + print(f" {m['key']}") + return 1 + else: + print("ALL IDENTICAL — verification passed.") + return 0 + + +def cmd_timing(args): + """Timing benchmark on largest cases.""" + n_cases = args.max_cases or 20 + cases = get_cases(n_cases) + modes = args.modes.split(",") if args.modes else MODES + thread_counts = [int(x) for x in args.threads.split(",")] + + print(f"Timing benchmark: {len(cases)} cases × {len(modes)} modes × {len(thread_counts)} thread configs") + + results = {} + + with tempfile.TemporaryDirectory() as tmpdir: + for mode in modes: + print(f"\n=== Mode: {mode} ===") + for n_threads in thread_counts: + total_time = 0.0 + for case in cases: + out_path = os.path.join(tmpdir, f"{case['name']}_{mode}_{n_threads}.fa") + _, elapsed = run_alignment(case, mode, n_threads, out_path) + total_time += elapsed + + key = f"{mode}:{n_threads}" + results[key] = round(total_time, 2) + + baseline_key = f"{mode}:{thread_counts[0]}" + baseline_time = results.get(baseline_key, total_time) + speedup = baseline_time / total_time if total_time > 0 else 0 + + print(f" {n_threads:2d} threads: {total_time:7.1f}s total (speedup: {speedup:.2f}x)") + + # Print summary table + print(f"\n{'='*60}") + print(f"{'Mode':<12s}", end="") + for t in thread_counts: + print(f" {t:>2d}T", end="") + print(" speedup") + print("-" * (12 + 5 * len(thread_counts) + 10)) + for mode in modes: + print(f"{mode:<12s}", end="") + baseline = results.get(f"{mode}:{thread_counts[0]}", 1) + for t in thread_counts: + val = results.get(f"{mode}:{t}", 0) + print(f" {val:>4.0f}", end="") + last = results.get(f"{mode}:{thread_counts[-1]}", 1) + print(f" {baseline/last:.2f}x") + + +def main(): + parser = argparse.ArgumentParser(description="BAliBASE verification and timing") + sub = parser.add_subparsers(dest="command", required=True) + + p_base = sub.add_parser("baseline", help="Generate baseline checksums") + p_base.add_argument("--output", required=True, help="Output JSON file") + p_base.add_argument("--modes", default=None, help="Comma-separated modes (default: fast,default,accurate)") + p_base.add_argument("--max-cases", type=int, default=0, help="Limit number of cases (0=all)") + p_base.add_argument("-v", "--verbose", action="store_true") + + p_cmp = sub.add_parser("compare", help="Compare against baseline") + p_cmp.add_argument("--baseline", required=True, help="Baseline JSON file") + p_cmp.add_argument("--threads", type=int, default=1, help="Thread count for comparison run") + p_cmp.add_argument("-v", "--verbose", action="store_true") + + p_time = sub.add_parser("timing", help="Timing benchmark") + p_time.add_argument("--threads", default="1,4,8,16", help="Comma-separated thread counts") + p_time.add_argument("--modes", default=None, help="Comma-separated modes") + p_time.add_argument("--max-cases", type=int, default=20, help="Number of largest cases") + + args = parser.parse_args() + + if not BB_ROOT.exists(): + print(f"ERROR: BAliBASE not found at {BB_ROOT}") + print("Run: uv run python -m benchmarks --download-only --dataset balibase") + return 1 + + if args.command == "baseline": + return cmd_baseline(args) + elif args.command == "compare": + return cmd_compare(args) + elif args.command == "timing": + return cmd_timing(args) + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/tests/dssim_test.c b/tests/dssim_test.c index 2c68d19..82ce3e4 100644 --- a/tests/dssim_test.c +++ b/tests/dssim_test.c @@ -63,8 +63,11 @@ int test_consistency(int num_tests, int numseq,int dna,int seed) msa_cpy(&m2, m); msa_shuffle_seq(m, rng); msa_shuffle_seq(m2, rng); - kalign_run(m, t1, KALIGN_TYPE_UNDEFINED,0.0,0.0,0.0, 0, 0); - kalign_run(m2, t2, KALIGN_TYPE_UNDEFINED,0.0,0.0,0.0, 0, 0); + { + struct kalign_run_config cfg = kalign_run_config_defaults(); + kalign_align_full(m, &cfg, 1, NULL, t1); + kalign_align_full(m2, &cfg, 1, NULL, t2); + } kalign_msa_compare(m, m2, &score); if(score != 100.0f){ diff --git a/tests/kalign_api_test.c b/tests/kalign_api_test.c index 8b107d6..fae60e6 100644 --- a/tests/kalign_api_test.c +++ b/tests/kalign_api_test.c @@ -1,17 +1,17 @@ /* * kalign_api_test.c — comprehensive tests for the kalign public C API. * - * Covers the functions not exercised by the existing test suite: - * - kalign_run_seeded() (VSM, consistency, tree seed/noise) - * - kalign_run_dist_scale() (VSM, seq_weights) - * - kalign_run_realign() (realign iterations) - * - kalign_post_realign() (post-align realign) - * - kalign_run() with refine modes + * Tests the unified kalign_align_full entry point with various configs: + * - Single run with refine modes + * - VSM + seq_weights via run config + * - Seeded tree + consistency anchors + * - Realign iterations + * - Ensemble alignment * - kalign_msa_compare_detailed() * - kalign_msa_compare_with_mask() * - kalign_check_msa() * - reformat_settings_msa() - * - kalign_write_msa() round-trip fasta + * - kalign_write_msa() round-trip fasta * * Each test: * 1. Reads input from the file passed as argv[1] @@ -36,7 +36,6 @@ /* helpers */ /* ------------------------------------------------------------------ */ -/* Count non-gap characters in an aligned sequence string. */ static int count_residues(const char *seq) { int n = 0; @@ -46,7 +45,6 @@ static int count_residues(const char *seq) return n; } -/* Record the ungapped lengths of all sequences before alignment. */ static int *snapshot_lengths(struct msa *m) { int *lens = malloc(sizeof(int) * m->numseq); @@ -57,11 +55,6 @@ static int *snapshot_lengths(struct msa *m) return lens; } -/* Verify basic alignment invariants: - * - alnlen > 0 - * - every seq string has length == alnlen - * - every seq preserves its original residue count - * Returns 0 on success, -1 on failure. */ static int verify_alignment(struct msa *m, int *orig_lens, const char *label) { if (m->alnlen <= 0) { @@ -91,7 +84,6 @@ static int verify_alignment(struct msa *m, int *orig_lens, const char *label) return 0; } -/* Read input, returning a fresh MSA. Caller must free with kalign_free_msa. */ static struct msa *read_input(const char *path) { struct msa *m = NULL; @@ -102,13 +94,19 @@ static struct msa *read_input(const char *path) return m; } +/* Helper: run a single alignment with default config */ +static int align_default(struct msa *msa) +{ + struct kalign_run_config cfg = kalign_run_config_defaults(); + return kalign_align_full(msa, &cfg, 1, NULL, 1); +} + /* ------------------------------------------------------------------ */ /* individual test functions */ /* ------------------------------------------------------------------ */ static int test_run_with_refine(const char *input) { - /* Test KALIGN_REFINE_ALL and KALIGN_REFINE_CONFIDENT */ int modes[] = {KALIGN_REFINE_ALL, KALIGN_REFINE_CONFIDENT}; const char *names[] = {"REFINE_ALL", "REFINE_CONFIDENT"}; @@ -117,9 +115,11 @@ static int test_run_with_refine(const char *input) if (!msa) return -1; int *lens = snapshot_lengths(msa); - int rv = kalign_run(msa, 1, -1, -1, -1, -1, modes[m], 0); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.refine = modes[m]; + int rv = kalign_align_full(msa, &cfg, 1, NULL, 1); if (rv != 0) { - fprintf(stderr, " kalign_run(%s) returned %d\n", names[m], rv); + fprintf(stderr, " kalign_align_full(%s) returned %d\n", names[m], rv); free(lens); kalign_free_msa(msa); return -1; @@ -142,11 +142,12 @@ static int test_run_dist_scale(const char *input) if (!msa) return -1; int *lens = snapshot_lengths(msa); - /* vsm_amax=2.0, seq_weights=1.0 */ - int rv = kalign_run_dist_scale(msa, 1, -1, -1, -1, -1, 0, 0, - 0.0f, 2.0f, 1.0f); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.vsm_amax = 2.0f; + cfg.seq_weights = 1.0f; + int rv = kalign_align_full(msa, &cfg, 1, NULL, 1); if (rv != 0) { - fprintf(stderr, " kalign_run_dist_scale returned %d\n", rv); + fprintf(stderr, " align_full(vsm+sw) returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; @@ -168,11 +169,14 @@ static int test_run_seeded(const char *input) if (!msa) return -1; int *lens = snapshot_lengths(msa); - /* tree_seed=42, tree_noise=0, vsm_amax=2.0, consistency_anchors=5 */ - int rv = kalign_run_seeded(msa, 1, -1, -1, -1, -1, 0, 0, - 42, 0.0f, 0.0f, 2.0f, 0.0f, 5, 2.0f); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.tree_seed = 42; + cfg.vsm_amax = 2.0f; + cfg.consistency_anchors = 5; + cfg.consistency_weight = 2.0f; + int rv = kalign_align_full(msa, &cfg, 1, NULL, 1); if (rv != 0) { - fprintf(stderr, " kalign_run_seeded returned %d\n", rv); + fprintf(stderr, " align_full(seeded+consistency) returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; @@ -194,11 +198,12 @@ static int test_run_realign(const char *input) if (!msa) return -1; int *lens = snapshot_lengths(msa); - /* realign_iterations=1, vsm_amax=2.0 */ - int rv = kalign_run_realign(msa, 1, -1, -1, -1, -1, 0, 0, - 0.0f, 2.0f, 1, 0.0f, 0, 2.0f); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.vsm_amax = 2.0f; + cfg.realign = 1; + int rv = kalign_align_full(msa, &cfg, 1, NULL, 1); if (rv != 0) { - fprintf(stderr, " kalign_run_realign returned %d\n", rv); + fprintf(stderr, " align_full(realign=1) returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; @@ -214,53 +219,26 @@ static int test_run_realign(const char *input) return 0; } -static int test_post_realign(const char *input) -{ - /* First do a normal alignment, then post-realign the result */ - struct msa *msa = read_input(input); - if (!msa) return -1; - int *lens = snapshot_lengths(msa); - - int rv = kalign_run(msa, 1, -1, -1, -1, -1, 0, 0); - if (rv != 0) { - fprintf(stderr, " initial kalign_run returned %d\n", rv); - free(lens); - kalign_free_msa(msa); - return -1; - } - - rv = kalign_post_realign(msa, 1, -1, -1, -1, -1, 0, 0, - 0.0f, 0.0f, 1, 0.0f); - if (rv != 0) { - fprintf(stderr, " kalign_post_realign returned %d\n", rv); - free(lens); - kalign_free_msa(msa); - return -1; - } - if (verify_alignment(msa, lens, "post_realign") != 0) { - free(lens); - kalign_free_msa(msa); - return -1; - } - fprintf(stdout, " post_realign: OK (alnlen=%d)\n", msa->alnlen); - free(lens); - kalign_free_msa(msa); - return 0; -} - static int test_ensemble_with_realign(const char *input) { struct msa *msa = read_input(input); if (!msa) return -1; int *lens = snapshot_lengths(msa); - /* ensemble=3, vsm=2.0, realign=1, refine=CONFIDENT */ - int rv = kalign_ensemble(msa, 1, -1, 3, -1.0f, -1.0f, -1.0f, - 42, 0, NULL, - KALIGN_REFINE_CONFIDENT, 0.0f, 2.0f, - 1, 0.0f, 0, 2.0f); + /* 3-run ensemble with realign and refine */ + struct kalign_run_config runs[3]; + for (int i = 0; i < 3; i++) { + runs[i] = kalign_run_config_defaults(); + runs[i].vsm_amax = 2.0f; + runs[i].realign = 1; + runs[i].refine = KALIGN_REFINE_CONFIDENT; + runs[i].tree_seed = 42 + (uint64_t)i; + runs[i].tree_noise = (i > 0) ? 0.2f : 0.0f; + } + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + int rv = kalign_align_full(msa, runs, 3, &ens, 1); if (rv != 0) { - fprintf(stderr, " kalign_ensemble+realign returned %d\n", rv); + fprintf(stderr, " align_full(ensemble+realign) returned %d\n", rv); free(lens); kalign_free_msa(msa); return -1; @@ -270,7 +248,7 @@ static int test_ensemble_with_realign(const char *input) kalign_free_msa(msa); return -1; } - /* Also check col_confidence */ + /* Check col_confidence */ if (msa->col_confidence == NULL) { fprintf(stderr, " [ens+realign] FAIL: col_confidence is NULL\n"); free(lens); @@ -294,13 +272,12 @@ static int test_ensemble_with_realign(const char *input) static int test_compare_detailed(const char *input) { - /* Align, then compare to self — should get perfect scores */ struct msa *ref = read_input(input); struct msa *test_msa = read_input(input); if (!ref || !test_msa) return -1; - kalign_run(ref, 1, -1, -1, -1, -1, 0, 0); - kalign_run(test_msa, 1, -1, -1, -1, -1, 0, 0); + align_default(ref); + align_default(test_msa); struct poar_score out; memset(&out, 0, sizeof(out)); @@ -312,7 +289,6 @@ static int test_compare_detailed(const char *input) return -1; } - /* Self-comparison: recall, precision, f1 should all be 1.0 */ if (fabs(out.recall - 1.0) > 0.001) { fprintf(stderr, " FAIL: recall=%.4f (expected 1.0)\n", out.recall); kalign_free_msa(ref); @@ -365,10 +341,9 @@ static int test_compare_with_mask(const char *input) struct msa *test_msa = read_input(input); if (!ref || !test_msa) return -1; - kalign_run(ref, 1, -1, -1, -1, -1, 0, 0); - kalign_run(test_msa, 1, -1, -1, -1, -1, 0, 0); + align_default(ref); + align_default(test_msa); - /* Create a mask that includes all columns */ int n_cols = ref->alnlen; int *mask = malloc(sizeof(int) * n_cols); if (!mask) { @@ -389,7 +364,6 @@ static int test_compare_with_mask(const char *input) return -1; } - /* All columns masked in → same as full comparison → perfect scores */ if (fabs(out.recall - 1.0) > 0.001 || fabs(out.precision - 1.0) > 0.001) { fprintf(stderr, " FAIL: mask-all recall=%.4f prec=%.4f\n", out.recall, out.precision); free(mask); @@ -398,7 +372,7 @@ static int test_compare_with_mask(const char *input) return -1; } - /* Now test with partial mask (first half only) */ + /* Test with partial mask */ for (int i = n_cols / 2; i < n_cols; i++) mask[i] = 0; memset(&out, 0, sizeof(out)); rv = kalign_msa_compare_with_mask(ref, test_msa, mask, n_cols, &out); @@ -409,7 +383,6 @@ static int test_compare_with_mask(const char *input) kalign_free_msa(test_msa); return -1; } - /* Partial mask self-compare should still give perfect scores */ if (fabs(out.recall - 1.0) > 0.001) { fprintf(stderr, " FAIL: partial mask recall=%.4f\n", out.recall); free(mask); @@ -430,8 +403,6 @@ static int test_check_msa(const char *input) struct msa *msa = read_input(input); if (!msa) return -1; - /* kalign_check_msa checks for duplicate sequences. - * Our test files shouldn't have exact duplicates. */ msa->quiet = 1; int rv = kalign_check_msa(msa, 0); if (rv != 0) { @@ -449,22 +420,19 @@ static int test_reformat_settings(const char *input) struct msa *msa = read_input(input); if (!msa) return -1; - /* First align so we have gaps */ - int rv = kalign_run(msa, 1, -1, -1, -1, -1, 0, 0); + int rv = align_default(msa); if (rv != 0) { fprintf(stderr, " initial align failed\n"); kalign_free_msa(msa); return -1; } - /* Test rename */ rv = reformat_settings_msa(msa, 1, 0); if (rv != 0) { fprintf(stderr, " reformat_settings_msa(rename) returned %d\n", rv); kalign_free_msa(msa); return -1; } - /* Verify names were changed to SEQ1, SEQ2, etc. */ for (int i = 0; i < msa->numseq; i++) { char expected[32]; snprintf(expected, sizeof(expected), "SEQ%d", i + 1); @@ -477,16 +445,12 @@ static int test_reformat_settings(const char *input) } fprintf(stdout, " reformat rename: OK\n"); - /* Test unalign — zeroes the gaps[] array and sets status to unaligned. - * Note: dealign_msa operates on the internal gaps[] representation, - * not on seq->seq (which holds finalised text with '-' chars). */ rv = reformat_settings_msa(msa, 0, 1); if (rv != 0) { fprintf(stderr, " reformat_settings_msa(unalign) returned %d\n", rv); kalign_free_msa(msa); return -1; } - /* After unalign, all gaps[] entries should be zero */ for (int i = 0; i < msa->numseq; i++) { for (int j = 0; j <= msa->sequences[i]->len; j++) { if (msa->sequences[i]->gaps[j] != 0) { @@ -505,11 +469,10 @@ static int test_reformat_settings(const char *input) static int test_write_roundtrip(const char *input) { - /* Align, write to fasta, read back, compare */ struct msa *msa = read_input(input); if (!msa) return -1; - int rv = kalign_run(msa, 1, -1, -1, -1, -1, 0, 0); + int rv = align_default(msa); if (rv != 0) { kalign_free_msa(msa); return -1; @@ -523,7 +486,6 @@ static int test_write_roundtrip(const char *input) return -1; } - /* Read it back */ struct msa *msa2 = NULL; rv = kalign_read_input((char *)tmpfile, &msa2, 1); if (rv != 0 || msa2 == NULL) { @@ -533,7 +495,6 @@ static int test_write_roundtrip(const char *input) return -1; } - /* Compare: same number of sequences, same alignment content */ if (msa->numseq != msa2->numseq) { fprintf(stderr, " FAIL: numseq %d vs %d\n", msa->numseq, msa2->numseq); kalign_free_msa(msa); @@ -542,7 +503,6 @@ static int test_write_roundtrip(const char *input) return -1; } - /* Use kalign_msa_compare for a real score check */ float score = 0; rv = kalign_msa_compare(msa, msa2, &score); if (rv != 0) { @@ -586,17 +546,16 @@ int main(int argc, char *argv[]) const char *name; int (*fn)(const char *); } tests[] = { - {"kalign_run + refine", test_run_with_refine}, - {"kalign_run_dist_scale (VSM)", test_run_dist_scale}, - {"kalign_run_seeded (consistency)", test_run_seeded}, - {"kalign_run_realign", test_run_realign}, - {"kalign_post_realign", test_post_realign}, - {"kalign_ensemble + realign", test_ensemble_with_realign}, - {"kalign_msa_compare_detailed", test_compare_detailed}, - {"kalign_msa_compare_with_mask", test_compare_with_mask}, - {"kalign_check_msa", test_check_msa}, - {"reformat_settings_msa", test_reformat_settings}, - {"write fasta roundtrip", test_write_roundtrip}, + {"kalign_align_full + refine", test_run_with_refine}, + {"VSM + seq_weights", test_run_dist_scale}, + {"seeded tree + consistency", test_run_seeded}, + {"realign iterations", test_run_realign}, + {"ensemble + realign", test_ensemble_with_realign}, + {"kalign_msa_compare_detailed", test_compare_detailed}, + {"kalign_msa_compare_with_mask", test_compare_with_mask}, + {"kalign_check_msa", test_check_msa}, + {"reformat_settings_msa", test_reformat_settings}, + {"write fasta roundtrip", test_write_roundtrip}, }; int n_tests = (int)(sizeof(tests) / sizeof(tests[0])); diff --git a/tests/kalign_ensemble_test.c b/tests/kalign_ensemble_test.c index df83a21..8d54fcb 100644 --- a/tests/kalign_ensemble_test.c +++ b/tests/kalign_ensemble_test.c @@ -55,6 +55,19 @@ int main(int argc, char *argv[]) return ret; } +/* Helper: create a 3-run ensemble config with default params */ +static void make_ensemble_runs(struct kalign_run_config *runs, int n_runs, + struct kalign_ensemble_config *ens, int min_support) +{ + for(int i = 0; i < n_runs; i++){ + runs[i] = kalign_run_config_defaults(); + runs[i].tree_seed = 42 + (uint64_t)i; + runs[i].tree_noise = (i > 0) ? 0.2f : 0.0f; + } + *ens = kalign_ensemble_config_defaults(); + ens->min_support = min_support; +} + /* Test 1: Run ensemble alignment with n_runs=3 and verify that * col_confidence is populated with values in [0, 1]. */ static int test_ensemble_confidence(const char *input_file) @@ -68,29 +81,29 @@ static int test_ensemble_confidence(const char *input_file) return -1; } - /* n_runs=3, default gap penalties, seed=42, min_support=0 (auto), no POAR save */ - rv = kalign_ensemble(msa, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 0, NULL, 0, 0.0f, -1.0f, 0, 0.0f, 0, 2.0f); + struct kalign_run_config runs[3]; + struct kalign_ensemble_config ens; + make_ensemble_runs(runs, 3, &ens, 0); + + rv = kalign_align_full(msa, runs, 3, &ens, 1); if(rv != 0){ - fprintf(stderr, " ERROR: kalign_ensemble returned %d\n", rv); + fprintf(stderr, " ERROR: kalign_align_full (ensemble) returned %d\n", rv); kalign_free_msa(msa); return -1; } - /* Check that col_confidence was allocated */ if(msa->col_confidence == NULL){ fprintf(stderr, " ERROR: col_confidence is NULL after ensemble\n"); kalign_free_msa(msa); return -1; } - /* Verify alignment length is positive */ if(msa->alnlen <= 0){ fprintf(stderr, " ERROR: alnlen is %d (expected > 0)\n", msa->alnlen); kalign_free_msa(msa); return -1; } - /* Check that all col_confidence values are in [0, 1] */ for(int i = 0; i < msa->alnlen; i++){ float c = msa->col_confidence[i]; if(c < 0.0f || c > 1.0f){ @@ -102,7 +115,6 @@ static int test_ensemble_confidence(const char *input_file) fprintf(stdout, " col_confidence: %d values, all in [0,1]\n", msa->alnlen); - /* Also verify that sequences are aligned (have equal length = alnlen) */ for(int i = 0; i < msa->numseq; i++){ if(msa->sequences[i]->seq == NULL){ fprintf(stderr, " ERROR: sequence %d has NULL seq\n", i); @@ -124,75 +136,58 @@ static int test_ensemble_confidence(const char *input_file) return 0; } -/* Test 2: Run ensemble with save_poar, then load POAR with - * kalign_consensus_from_poar and verify both produce aligned output. */ +/* Test 2: Run ensemble with min_support=2, save POAR, then load POAR with + * kalign_consensus_from_poar and verify both produce matching aligned output. + * + * NOTE: POAR save is no longer in the ensemble config. This test now verifies + * that two ensemble runs with the same params produce identical alignments + * when using explicit min_support (deterministic consensus path). */ static int test_poar_round_trip(const char *input_file) { struct msa *msa1 = NULL; struct msa *msa2 = NULL; int rv; - const char *poar_path = "test_ensemble_poar.bin"; - /* First run: ensemble with POAR save */ rv = kalign_read_input((char *)input_file, &msa1, 1); if(rv != 0 || msa1 == NULL){ fprintf(stderr, " ERROR: failed to read input file: %s\n", input_file); return -1; } - /* Use explicit min_support=2 so the ensemble always takes the consensus - * path. kalign_consensus_from_poar() also requires min_support >= 1, - * and both must use the same threshold for the output to match. */ - rv = kalign_ensemble(msa1, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 2, poar_path, 0, 0.0f, -1.0f, 0, 0.0f, 0, 2.0f); - if(rv != 0){ - fprintf(stderr, " ERROR: kalign_ensemble (save) returned %d\n", rv); - kalign_free_msa(msa1); - return -1; - } - - /* Verify the POAR file was created */ - FILE *fp = fopen(poar_path, "rb"); - if(fp == NULL){ - fprintf(stderr, " ERROR: POAR file was not created: %s\n", poar_path); - kalign_free_msa(msa1); - return -1; - } - fclose(fp); - - fprintf(stdout, " POAR saved to %s\n", poar_path); - - /* Second run: load POAR and derive consensus */ rv = kalign_read_input((char *)input_file, &msa2, 1); if(rv != 0 || msa2 == NULL){ - fprintf(stderr, " ERROR: failed to read input for POAR load\n"); + fprintf(stderr, " ERROR: failed to read input (2nd copy)\n"); kalign_free_msa(msa1); return -1; } - rv = kalign_consensus_from_poar(msa2, poar_path, 2); + struct kalign_run_config runs[3]; + struct kalign_ensemble_config ens; + make_ensemble_runs(runs, 3, &ens, 2); + + rv = kalign_align_full(msa1, runs, 3, &ens, 1); if(rv != 0){ - fprintf(stderr, " ERROR: kalign_consensus_from_poar returned %d\n", rv); + fprintf(stderr, " ERROR: kalign_align_full (run 1) returned %d\n", rv); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } - /* Verify both MSAs have valid aligned sequences */ - if(msa1->alnlen <= 0){ - fprintf(stderr, " ERROR: msa1 alnlen = %d\n", msa1->alnlen); + rv = kalign_align_full(msa2, runs, 3, &ens, 1); + if(rv != 0){ + fprintf(stderr, " ERROR: kalign_align_full (run 2) returned %d\n", rv); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } - if(msa2->alnlen <= 0){ - fprintf(stderr, " ERROR: msa2 alnlen = %d\n", msa2->alnlen); + if(msa1->alnlen <= 0 || msa2->alnlen <= 0){ + fprintf(stderr, " ERROR: alnlen msa1=%d msa2=%d\n", msa1->alnlen, msa2->alnlen); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } - /* Both should have the same number of sequences */ if(msa1->numseq != msa2->numseq){ fprintf(stderr, " ERROR: numseq mismatch: %d vs %d\n", msa1->numseq, msa2->numseq); @@ -201,7 +196,6 @@ static int test_poar_round_trip(const char *input_file) return -1; } - /* Both should have the same alignment length (same POAR, same consensus) */ if(msa1->alnlen != msa2->alnlen){ fprintf(stderr, " ERROR: alnlen mismatch: %d vs %d\n", msa1->alnlen, msa2->alnlen); @@ -210,33 +204,20 @@ static int test_poar_round_trip(const char *input_file) return -1; } - /* Verify aligned sequences match between direct ensemble and POAR-loaded consensus */ for(int i = 0; i < msa1->numseq; i++){ - if(msa1->sequences[i]->seq == NULL || msa2->sequences[i]->seq == NULL){ - fprintf(stderr, " ERROR: NULL seq at index %d\n", i); - kalign_free_msa(msa1); - kalign_free_msa(msa2); - return -1; - } if(strcmp(msa1->sequences[i]->seq, msa2->sequences[i]->seq) != 0){ - fprintf(stderr, " ERROR: sequence %d mismatch between ensemble and POAR load\n", i); - fprintf(stderr, " direct: %.60s...\n", msa1->sequences[i]->seq); - fprintf(stderr, " loaded: %.60s...\n", msa2->sequences[i]->seq); + fprintf(stderr, " ERROR: sequence %d mismatch between runs\n", i); kalign_free_msa(msa1); kalign_free_msa(msa2); return -1; } } - fprintf(stdout, " Round-trip: %d sequences, alnlen=%d, consensus matches\n", + fprintf(stdout, " Deterministic: %d sequences, alnlen=%d, both runs match\n", msa1->numseq, msa1->alnlen); kalign_free_msa(msa1); kalign_free_msa(msa2); - - /* Clean up temp file */ - remove(poar_path); - return 0; } @@ -252,22 +233,23 @@ static int test_min_support(const char *input_file) return -1; } - /* min_support=2 (explicit), n_runs=3 */ - rv = kalign_ensemble(msa, 1, -1, 3, -1.0f, -1.0f, -1.0f, 42, 2, NULL, 0, 0.0f, -1.0f, 0, 0.0f, 0, 2.0f); + struct kalign_run_config runs[3]; + struct kalign_ensemble_config ens; + make_ensemble_runs(runs, 3, &ens, 2); + + rv = kalign_align_full(msa, runs, 3, &ens, 1); if(rv != 0){ - fprintf(stderr, " ERROR: kalign_ensemble with min_support=2 returned %d\n", rv); + fprintf(stderr, " ERROR: kalign_align_full with min_support=2 returned %d\n", rv); kalign_free_msa(msa); return -1; } - /* Verify alignment was produced */ if(msa->alnlen <= 0){ fprintf(stderr, " ERROR: alnlen is %d (expected > 0)\n", msa->alnlen); kalign_free_msa(msa); return -1; } - /* Verify sequences are aligned */ for(int i = 0; i < msa->numseq; i++){ if(msa->sequences[i]->seq == NULL){ fprintf(stderr, " ERROR: sequence %d has NULL seq\n", i); @@ -283,7 +265,6 @@ static int test_min_support(const char *input_file) } } - /* Verify col_confidence is populated */ if(msa->col_confidence == NULL){ fprintf(stderr, " ERROR: col_confidence is NULL with min_support=2\n"); kalign_free_msa(msa); diff --git a/tests/kalign_lib_test.c b/tests/kalign_lib_test.c index a7f1081..b6f0f14 100644 --- a/tests/kalign_lib_test.c +++ b/tests/kalign_lib_test.c @@ -15,8 +15,11 @@ int main(int argc, char *argv[]) fprintf(stdout,"reading from %s\n", argv[i]); kalign_read_input(argv[i], &msa,1); } - /* Align seqences */ - kalign_run(msa,1 , -1, -1, -1 , -1, 0, 0); + /* Align sequences */ + { + struct kalign_run_config cfg = kalign_run_config_defaults(); + kalign_align_full(msa, &cfg, 1, NULL, 1); + } /* write alignment in clustal format */ kalign_write_msa(msa, "test.clu", "clu"); /* write alignment in aligned fasta format */ diff --git a/tests/large_benchmark.c b/tests/large_benchmark.c index 9224787..1c2738a 100644 --- a/tests/large_benchmark.c +++ b/tests/large_benchmark.c @@ -177,7 +177,10 @@ int run_test_aln(struct aln_case *tcase) } /* reference */ RUN(kalign_read_input(path, &r,1)); - kalign_run(t,16 , -1, -1, -1 , -1, 0, 0); + { + struct kalign_run_config cfg = kalign_run_config_defaults(); + kalign_align_full(t, &cfg, 1, NULL, 16); + } diff --git a/tests/memcheck_stress.c b/tests/memcheck_stress.c index 2cf9ce9..2d1532f 100644 --- a/tests/memcheck_stress.c +++ b/tests/memcheck_stress.c @@ -2,7 +2,7 @@ * * Exercises all major code paths repeatedly to surface memory bugs: * - In-memory alignment (kalign_arr_to_msa path) - * - File-based alignment (kalign_read_input path) + * - File-based alignment (kalign_align_full path) * - Realignment iterations * - Refinement (confident, inline) * - Ensemble alignment with consensus @@ -10,20 +10,6 @@ * - Consistency anchors * - VSM + seq_weights * - Align + write + read-back + compare (full benchmark loop) - * - * Compile with ASAN: - * cc -fsanitize=address -O0 -g -DDEBUG \ - * -I../lib/include -I../lib/src \ - * memcheck_stress.c \ - * -L../build-asan/lib -lkalign_static -ltldevel \ - * -fopenmp -lm -o memcheck_stress - * - * Compile for Valgrind: - * cc -O0 -g -DDEBUG \ - * -I../lib/include -I../lib/src \ - * memcheck_stress.c \ - * -L../build-debug/lib -lkalign_static -ltldevel \ - * -fopenmp -lm -o memcheck_stress */ #include @@ -97,7 +83,8 @@ static int test_file_align(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) { fprintf(stderr, " read failed iter %d\n", i); return 1; } msa->quiet = 1; - ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, KALIGN_REFINE_NONE, 0); + struct kalign_run_config cfg = kalign_run_config_defaults(); + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { fprintf(stderr, " align failed iter %d\n", i); kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -112,10 +99,9 @@ static int test_realign(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_run_realign(msa, 1, KALIGN_TYPE_UNDEFINED, - -1.0f, -1.0f, -1.0f, - KALIGN_REFINE_NONE, 0, - 0.0f, -1.0f, 1, -1.0f, 0, 2.0f); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.realign = 1; + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -130,8 +116,9 @@ static int test_refine(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, - KALIGN_REFINE_CONFIDENT, 0); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.refine = KALIGN_REFINE_CONFIDENT; + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -184,11 +171,14 @@ static int test_ensemble(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_ensemble(msa, 1, KALIGN_TYPE_UNDEFINED, - 3, -1.0f, -1.0f, -1.0f, - 42, 0, NULL, - KALIGN_REFINE_NONE, 0.0f, -1.0f, - 0, -1.0f, 0, 2.0f); + struct kalign_run_config runs[3]; + for (int k = 0; k < 3; k++) { + runs[k] = kalign_run_config_defaults(); + runs[k].tree_seed = 42 + (uint64_t)k; + runs[k].tree_noise = (k > 0) ? 0.2f : 0.0f; + } + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + ret = kalign_align_full(msa, runs, 3, &ens, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -204,8 +194,8 @@ static int test_benchmark_loop(const char* input, const char* ref_file, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, - KALIGN_REFINE_NONE, 0); + struct kalign_run_config cfg = kalign_run_config_defaults(); + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } ret = kalign_write_msa(msa, tmpfile, "fasta"); kalign_free_msa(msa); @@ -247,11 +237,10 @@ static int test_consistency(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_run_seeded(msa, 1, KALIGN_TYPE_UNDEFINED, - -1.0f, -1.0f, -1.0f, - KALIGN_REFINE_NONE, 0, - 0, 0.0f, 0.0f, -1.0f, -1.0f, - 3, 2.0f); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.consistency_anchors = 3; + cfg.consistency_weight = 2.0f; + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -266,11 +255,16 @@ static int test_ensemble_realign(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_ensemble(msa, 1, KALIGN_TYPE_UNDEFINED, - 3, -1.0f, -1.0f, -1.0f, - 42, 0, NULL, - KALIGN_REFINE_CONFIDENT, 0.0f, -1.0f, - 1, -1.0f, 0, 2.0f); + struct kalign_run_config runs[3]; + for (int k = 0; k < 3; k++) { + runs[k] = kalign_run_config_defaults(); + runs[k].realign = 1; + runs[k].refine = KALIGN_REFINE_CONFIDENT; + runs[k].tree_seed = 42 + (uint64_t)k; + runs[k].tree_noise = (k > 0) ? 0.2f : 0.0f; + } + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + ret = kalign_align_full(msa, runs, 3, &ens, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -285,11 +279,18 @@ static int test_ensemble_vsm_sw(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_ensemble(msa, 1, KALIGN_TYPE_UNDEFINED, - 3, -1.0f, -1.0f, -1.0f, - 42, 0, NULL, - KALIGN_REFINE_CONFIDENT, 0.0f, 2.0f, - 1, 1.0f, 0, 2.0f); + struct kalign_run_config runs[3]; + for (int k = 0; k < 3; k++) { + runs[k] = kalign_run_config_defaults(); + runs[k].vsm_amax = 2.0f; + runs[k].seq_weights = 1.0f; + runs[k].realign = 1; + runs[k].refine = KALIGN_REFINE_CONFIDENT; + runs[k].tree_seed = 42 + (uint64_t)k; + runs[k].tree_noise = (k > 0) ? 0.2f : 0.0f; + } + struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); + ret = kalign_align_full(msa, runs, 3, &ens, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -304,8 +305,9 @@ static int test_inline_refine(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_run(msa, 1, KALIGN_TYPE_UNDEFINED, -1.0f, -1.0f, -1.0f, - KALIGN_REFINE_INLINE, 0); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.refine = KALIGN_REFINE_INLINE; + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } @@ -330,11 +332,12 @@ static int test_param_sweep(const char* input, int n) int ret = kalign_read_input((char*)input, &msa, 1); if (ret != 0 || !msa) return 1; msa->quiet = 1; - ret = kalign_run_seeded(msa, 1, KALIGN_TYPE_UNDEFINED, - -1.0f, -1.0f, -1.0f, - KALIGN_REFINE_NONE, 0, - 0, 0.0f, 0.0f, vsm, sw, - cons, 2.0f); + struct kalign_run_config cfg = kalign_run_config_defaults(); + cfg.vsm_amax = vsm; + cfg.seq_weights = sw; + cfg.consistency_anchors = cons; + cfg.consistency_weight = 2.0f; + ret = kalign_align_full(msa, &cfg, 1, NULL, 1); if (ret != 0) { kalign_free_msa(msa); return 1; } kalign_free_msa(msa); } From 1002f0a560372b481bc81e37103f37c2c2860f10 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Thu, 26 Mar 2026 18:38:09 +0800 Subject: [PATCH 10/29] Add confidence masking for ensemble alignment output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose per-column ensemble confidence scores to users. Low-confidence columns can be masked (lowercase residues) or removed (replaced with gaps), useful for phylogenetics and structure prediction pipelines where uncertain alignment regions should be excluded. C library: - kalign_mask_by_confidence(msa, threshold, style) in msa_op.c - kalign_write_confidence(msa, path) for raw score output - Styles: KALIGN_MASK_LOWERCASE (default), KALIGN_MASK_REMOVE CLI: - --confidence-threshold FLOAT (0-1, requires ensemble mode) - --confidence-style (lowercase/remove) - --confidence-output FILE Python API: - kalign.mask_alignment(result, threshold, style) → AlignedSequences - kalign.filter_alignment(result, threshold) → AlignedSequences - kalign.write_confidence(path, result) Gracefully warns and skips when confidence is unavailable (non-ensemble modes). 11 Python tests covering all paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...RD-confidence-masking-and-add-sequences.md | 313 ++++++++++++++++++ lib/include/kalign/kalign.h | 7 + lib/src/msa_op.c | 77 +++++ lib/src/msa_op.h | 12 + python-kalign/__init__.py | 123 +++++++ scripts/bench_confidence_filtering.py | 162 +++++++++ src/parameters.c | 3 + src/parameters.h | 3 + src/run_kalign.c | 34 ++ tests/python/test_confidence.py | 122 +++++++ 10 files changed, 856 insertions(+) create mode 100644 docs/PRD-confidence-masking-and-add-sequences.md create mode 100644 scripts/bench_confidence_filtering.py create mode 100644 tests/python/test_confidence.py diff --git a/docs/PRD-confidence-masking-and-add-sequences.md b/docs/PRD-confidence-masking-and-add-sequences.md new file mode 100644 index 0000000..303fdac --- /dev/null +++ b/docs/PRD-confidence-masking-and-add-sequences.md @@ -0,0 +1,313 @@ +# PRD: Confidence masking + Add sequences to existing alignment + +## Feature 1: Confidence-filtered output + +### What + +Expose the per-column ensemble confidence scores to the user. Allow masking (lowercasing or gap-replacing) columns below a threshold. Only meaningful for ensemble modes (recall, accurate) which compute POAR confidence. + +### Why + +No other fast aligner provides per-column reliability. GUIDANCE2 does but is extremely slow (runs external aligners repeatedly). Kalign's ensemble already does the multiple runs — confidence is free. This turns kalign into a one-stop alignment + quality filter tool, directly useful for phylogenetics, positive selection, and structure prediction pipelines. + +### Paper angle + +**New figure or table:** Score BAliBASE alignments on ONLY the columns kalign considers confident (confidence >= threshold). Show that SP/TC on confident columns is dramatically higher than on all columns — proving the confidence scores are meaningful. Compare at several thresholds (0.3, 0.5, 0.7, 0.9). + +This directly addresses the TC weakness: "kalign's overall TC is lower than competitors, but if you trust its confidence scores and filter, the remaining columns have TC comparable to or better than MAFFT/MUSCLE." + +**Second comparison:** Run phylogenetic tree inference (IQ-TREE) on: +1. Full kalign accurate alignment +2. Kalign accurate filtered at confidence >= 0.5 +3. MAFFT alignment +4. MUSCLE alignment + +Compare Robinson-Foulds distance to the true tree (from INDELible simulations, which the manuscript already has). If filtered kalign gives better trees, that's a compelling result. + +### C CLI interface + +``` +kalign -i seqs.fa -o aligned.fa --mode accurate --confidence-threshold 0.7 + +Options: + --confidence-threshold FLOAT Mask columns with confidence below this value. + 0.0 = no masking (default). Requires ensemble mode. + --confidence-style STRING "lowercase" (default) or "remove". + lowercase: uncertain residues become lowercase + remove: uncertain columns replaced with gaps + --confidence-output FILE Write per-column confidence values to a separate file + (one float per line, one line per column) +``` + +The `--confidence-output` flag is useful for downstream tools that want the raw scores (e.g., custom trimming scripts). + +### Python API + +```python +# align() and align_from_file() already return confidence when using ensemble modes +result = kalign.align(sequences, mode="accurate") +# result.confidence is a list of per-column floats [0.0, 1.0] + +# New: mask_alignment convenience function +masked = kalign.mask_alignment(result, threshold=0.7, style="lowercase") +# masked.sequences has lowercase residues in low-confidence columns + +# New: filter_alignment removes low-confidence columns entirely +filtered = kalign.filter_alignment(result, threshold=0.7) +# filtered.sequences are shorter — only high-confidence columns remain + +# align_file_to_file gains optional confidence args +kalign.align_file_to_file( + "in.fa", "out.fa", + mode="accurate", + confidence_threshold=0.7, + confidence_style="lowercase", # or "remove" +) + +# Write raw confidence scores +kalign.write_confidence("confidence.txt", result) +``` + +### Implementation + +**C library changes:** + +1. `lib/src/msa_io.c` — modify `write_fasta` / `write_clustal` / `write_msf` to apply masking: + - Accept threshold + style parameters + - Before writing each character: if `col_confidence[col] < threshold`, apply style + - For "lowercase": `seq[col] = tolower(seq[col])` (only if not gap) + - For "remove": `seq[col] = '-'` + +2. `src/run_kalign.c` — add CLI flags: + - `--confidence-threshold` → `float conf_threshold` + - `--confidence-style` → `enum {CONF_LOWERCASE, CONF_REMOVE}` + - `--confidence-output` → write `msa->col_confidence[]` to file + - Validate: if threshold > 0 and mode is not ensemble, warn and ignore + +3. `python-kalign/__init__.py` — add `mask_alignment()`, `filter_alignment()`, `write_confidence()` +4. `python-kalign/_core.cpp` — pass threshold/style to `kalign_write_msa` or post-process in Python + +**Key constraint:** Confidence is only available after ensemble alignment (`col_confidence` is NULL for single-run modes). The CLI and Python API must handle this gracefully — warn if the user requests masking with fast/default mode. + +### Testing + +1. **Unit test:** Align BB11001 with accurate mode, verify `col_confidence` is populated, apply threshold=0.5, check that output has lowercase characters in the right columns + +2. **Round-trip test:** Write masked alignment, read it back, verify non-masked residues are unchanged + +3. **Quality test (for paper):** Score all 218 BAliBASE cases: + - Compute SP and TC on ALL columns (standard) + - Compute SP and TC on ONLY columns with confidence >= threshold + - Show improvement as threshold increases + - Script: `scripts/bench_confidence_filtering.py` + +4. **Phylogenetic test (for paper):** Use the manuscript's INDELible simulations: + - Align with kalign accurate + - Filter at various thresholds + - Run IQ-TREE + - Compare RF distance to true tree + - Already partially done in the manuscript pipeline + +--- + +## Feature 2: Add sequences to existing alignment + +### What + +Align new sequences against an existing (fixed) alignment without modifying the existing sequences. Each new sequence is independently aligned to the consensus profile of the existing alignment. + +### Why + +This is one of the most requested features in MSA tools. MAFFT `--add` is heavily cited specifically for this. Use cases: +- Metagenomics: add new sample sequences to a reference alignment +- Phylogenetics: add new taxa to a growing tree alignment +- Viral surveillance: daily additions to reference alignments (SARS-CoV-2, influenza) +- Database maintenance: adding sequences to curated family alignments + +### Paper angle + +**Benchmark against MAFFT --add:** + +1. Take BAliBASE reference alignments. For each case: + - Hold out 20% of sequences as "new" + - Use remaining 80% as the "existing alignment" + - Add the held-out sequences with kalign --add and mafft --add + - Score the added sequences against the full reference alignment + - Measure time + +2. Larger-scale test with simulated data: + - INDELible simulation with 500 sequences + - Use first 400 as reference alignment (align normally) + - Add remaining 100 with --add + - Compare to full 500-sequence alignment + - Time comparison: kalign --add vs mafft --add + +This gives both quality and speed comparisons. Kalign should be faster (SIMD Hirschberg). Quality depends on how well the seq-to-profile alignment places gaps. + +### C CLI interface + +``` +kalign --add new_seqs.fa --existing aligned.fa -o combined.fa + +Options: + --add FILE Unaligned sequences to add to an existing alignment + --existing FILE Existing alignment (FASTA/MSF/Clustal). These sequences + are NOT re-aligned — their gaps are preserved exactly. + -o FILE Output: existing sequences (unchanged) + new sequences + (with gaps inserted to fit the existing column structure) + --nthreads N Parallel seq-to-profile alignments for each new sequence +``` + +**Behavior:** +- Read existing alignment → build profile +- For each new sequence: align to profile, insert gaps +- Output: existing sequences verbatim + new sequences with gaps +- Existing sequences are NEVER modified +- Column count may increase if new sequences have insertions not present in the existing alignment (new gap columns inserted in ALL sequences at those positions) + +### Python API + +```python +# From files +kalign.add_to_alignment( + existing="reference.fa", + new_sequences="new.fa", + output="combined.fa", + n_threads=8, +) + +# In-memory +existing = kalign.align(ref_sequences, mode="accurate") +combined = kalign.add_sequences(existing, new_sequences, n_threads=8) +# combined.sequences = existing (unchanged) + new (gapped) +# combined.names = existing names + new names +``` + +### Implementation + +**C library:** + +1. **New public API function in `kalign.h`:** + ```c + int kalign_add_sequences(struct msa* existing_aln, + struct msa* new_seqs, + int n_threads); + ``` + - `existing_aln`: finalized alignment (sequences have gap chars, alnlen set) + - `new_seqs`: unaligned sequences + - After call: `existing_aln` contains original + new sequences, all aligned + - Returns OK/FAIL + +2. **Core algorithm in new file `lib/src/aln_add.c`:** + + ``` + kalign_add_sequences(existing, new_seqs, n_threads): + 1. Detect biotype, encode new sequences to internal alphabet + 2. Build consensus profile from existing alignment + - Walk all columns of existing alignment + - At each column: count residue frequencies (weighted by sequence count) + - Store as float[64] per column (same format as progressive profiles) + 3. For each new sequence (parallelizable with tp_parallel_for): + a. Run seq-to-profile Hirschberg alignment (aln_seqprofile) + b. Extract gap positions from alignment path + c. Insert gaps into new sequence to match existing column structure + d. If new sequence has insertions not in existing alignment: + - Record insertion positions and lengths + 4. If any insertions were found: + - Insert new gap columns into ALL sequences (existing + new) at those positions + - Update alnlen + 5. Append new sequences to existing MSA + ``` + +3. **Profile building from existing alignment:** + - Similar to what `make_profile_n` does during progressive alignment + - But operates on finalized character sequences with gap chars + - Convert back to internal representation for the DP + - Or: build profile directly from character frequencies per column + +4. **CLI in `src/run_kalign.c`:** + - Parse `--add` and `--existing` flags + - Read both files + - Call `kalign_add_sequences` + - Write combined output + +5. **Python bindings in `_core.cpp`:** + - New `add_sequences` function + - Reads existing alignment file, reads new sequences file, calls C API + +**Parallelism:** Each new sequence's alignment to the profile is independent → `tp_parallel_for` over new sequences. This is embarrassingly parallel and should scale well. + +**Key design decision — handling insertions in new sequences:** + +When a new sequence has residues that don't map to any existing column, we must insert new columns. This affects ALL sequences (existing ones get gaps at those positions). Two approaches: + +- **Strict (MAFFT --add style):** Never insert new columns. New sequence insertions are forced into existing columns or dropped. Existing alignment column count is preserved exactly. +- **Flexible:** Insert new columns as needed. More accurate for the new sequences but modifies the column structure. + +Recommend: **strict mode as default** (existing sequences completely untouched, even column count preserved), with a `--add-insertions` flag for flexible mode. The strict mode is what users expect from "add to existing alignment." + +In strict mode, the seq-to-profile alignment path may indicate insertions in the new sequence. These residues are simply lowercase or dropped (user choice). This matches MAFFT --add behavior. + +### Testing + +1. **Identity test:** Add a sequence that's already in the alignment. Result should be identical to the original. + +2. **Residue preservation test:** After adding, count non-gap characters in each new sequence — must equal original sequence length. + +3. **Existing-unchanged test:** After adding, the existing sequences must be byte-identical to the input existing alignment. + +4. **BAliBASE holdout test (for paper):** + - For each BAliBASE case: hold out 20% sequences + - Align 80% normally → existing alignment + - Add held-out 20% with kalign --add + - Score the full result against BAliBASE reference + - Compare to: MAFFT --add, and full kalign alignment of all sequences + +5. **Speed test (for paper):** + - 1000-sequence reference alignment + add 100 new sequences + - Time kalign --add vs mafft --add + - Repeat at 5000 + 500, 10000 + 1000 + +6. **Simulation test (for paper):** + - INDELible: generate true alignment of 500 sequences + - Split: 400 reference + 100 to add + - Align 400 with each tool + - Add 100 with each tool's --add + - Score against true alignment + - Also run full 500-sequence alignment as upper bound + +--- + +## Implementation order + +``` +Phase 1: Confidence masking (simpler, builds on existing infrastructure) + 1a. C library: masking function in msa_op.c + 1b. CLI: --confidence-threshold, --confidence-style, --confidence-output + 1c. Python: mask_alignment(), filter_alignment(), write_confidence() + 1d. Tests: unit + BAliBASE quality at thresholds + 1e. Paper benchmark: SP/TC on confident columns only + +Phase 2: Add sequences (more complex, new alignment mode) + 2a. C library: kalign_add_sequences in aln_add.c + 2b. Profile building from existing alignment + 2c. Seq-to-profile alignment + gap insertion + 2d. CLI: --add, --existing + 2e. Python: add_to_alignment(), add_sequences() + 2f. Tests: identity + preservation + unchanged + holdout + 2g. Paper benchmark: BAliBASE holdout + speed vs MAFFT --add +``` + +## Estimated complexity + +| Component | Lines of C | Difficulty | +|-----------|:----------:|:----------:| +| Confidence masking (msa_op.c) | ~50 | Easy | +| Confidence CLI flags | ~30 | Easy | +| Confidence Python API | ~80 | Easy | +| Confidence paper benchmark script | ~150 | Easy | +| Add-sequences core (aln_add.c) | ~300 | Medium | +| Add-sequences profile builder | ~150 | Medium | +| Add-sequences CLI | ~50 | Easy | +| Add-sequences Python API | ~100 | Easy | +| Add-sequences paper benchmark | ~200 | Medium | diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 86234b5..ee65e4e 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -69,6 +69,13 @@ EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); +/* Confidence masking */ +#define KALIGN_MASK_LOWERCASE 0 +#define KALIGN_MASK_REMOVE 1 + +EXTERN int kalign_mask_by_confidence(struct msa* msa, float threshold, int style); +EXTERN int kalign_write_confidence(struct msa* msa, const char* path); + /* Memory */ EXTERN void kalign_free_msa(struct msa* msa); diff --git a/lib/src/msa_op.c b/lib/src/msa_op.c index 6358b27..feec768 100644 --- a/lib/src/msa_op.c +++ b/lib/src/msa_op.c @@ -607,3 +607,80 @@ int kalign_msa_get_biotype(struct msa *msa) if(!msa) return ALN_BIOTYPE_UNDEF; return msa->biotype; } + +/* ======================================================================== */ +/* Confidence masking */ +/* ======================================================================== */ + +#define KALIGN_MASK_LOWERCASE 0 +#define KALIGN_MASK_REMOVE 1 + +int kalign_mask_by_confidence(struct msa* msa, float threshold, int style) +{ + int i, col; + + ASSERT(msa != NULL, "No MSA"); + + if(threshold <= 0.0f){ + return OK; /* no masking requested */ + } + + if(msa->col_confidence == NULL){ + WARNING_MSG("No confidence scores available (requires ensemble mode). " + "Skipping masking."); + return OK; + } + + ASSERT(msa->aligned == ALN_STATUS_FINAL, "MSA must be finalized"); + ASSERT(msa->alnlen > 0, "Alignment length is 0"); + + for(col = 0; col < msa->alnlen; col++){ + if(msa->col_confidence[col] >= threshold){ + continue; /* column is confident */ + } + /* Mask this column in all sequences */ + for(i = 0; i < msa->numseq; i++){ + char c = msa->sequences[i]->seq[col]; + if(c == '-') continue; /* gaps stay gaps */ + if(style == KALIGN_MASK_REMOVE){ + msa->sequences[i]->seq[col] = '-'; + }else{ + /* KALIGN_MASK_LOWERCASE */ + msa->sequences[i]->seq[col] = (char)tolower((unsigned char)c); + } + } + } + + return OK; +ERROR: + return FAIL; +} + +int kalign_write_confidence(struct msa* msa, const char* path) +{ + FILE* fp = NULL; + int col; + + ASSERT(msa != NULL, "No MSA"); + ASSERT(path != NULL, "No output path"); + + if(msa->col_confidence == NULL){ + WARNING_MSG("No confidence scores to write."); + return OK; + } + + fp = fopen(path, "w"); + if(!fp){ + ERROR_MSG("Cannot open %s for writing", path); + } + + for(col = 0; col < msa->alnlen; col++){ + fprintf(fp, "%.4f\n", msa->col_confidence[col]); + } + + fclose(fp); + return OK; +ERROR: + if(fp) fclose(fp); + return FAIL; +} diff --git a/lib/src/msa_op.h b/lib/src/msa_op.h index f424882..f0a1d92 100644 --- a/lib/src/msa_op.h +++ b/lib/src/msa_op.h @@ -32,6 +32,18 @@ EXTERN int kalign_arr_to_msa(char **input_sequences, int *len, int numseq, struc EXTERN int finalise_alignment(struct msa* msa); EXTERN int make_linear_sequence(struct msa_seq *seq, char *linear_seq); +/* Confidence masking styles */ +#define KALIGN_MASK_LOWERCASE 0 +#define KALIGN_MASK_REMOVE 1 + +/* Mask low-confidence alignment columns. + style: KALIGN_MASK_LOWERCASE (residues → lowercase) or KALIGN_MASK_REMOVE (→ gaps). + No-op if threshold <= 0 or col_confidence is NULL (non-ensemble modes). */ +EXTERN int kalign_mask_by_confidence(struct msa* msa, float threshold, int style); + +/* Write per-column confidence scores to a text file (one value per line). */ +EXTERN int kalign_write_confidence(struct msa* msa, const char* path); + #undef MSA_OP_IMPORT #undef EXTERN diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index 5ab0cec..17f7296 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -680,6 +680,126 @@ def align_file_to_file( kalign = align +# --------------------------------------------------------------------------- +# Confidence masking utilities +# --------------------------------------------------------------------------- + + +def mask_alignment( + result: AlignedSequences, + threshold: float = 0.5, + style: str = "lowercase", +) -> AlignedSequences: + """Mask low-confidence columns in an alignment result. + + Parameters + ---------- + result : AlignedSequences + Alignment result with confidence scores (from ensemble modes). + threshold : float + Columns with confidence below this value are masked. + style : str + "lowercase" — uncertain residues become lowercase (default). + "remove" — uncertain residues replaced with '-'. + + Returns + ------- + AlignedSequences + New result with masked sequences. Confidence arrays unchanged. + """ + if result.column_confidence is None: + import warnings + warnings.warn( + "No confidence scores available (requires ensemble mode). " + "Returning unmasked alignment." + ) + return result + + conf = result.column_confidence + masked_seqs = [] + for seq in result.sequences: + chars = list(seq) + for col in range(len(chars)): + if col < len(conf) and conf[col] < threshold and chars[col] != '-': + if style == "remove": + chars[col] = '-' + else: + chars[col] = chars[col].lower() + masked_seqs.append(''.join(chars)) + + return AlignedSequences( + names=result.names, + sequences=masked_seqs, + column_confidence=result.column_confidence, + residue_confidence=result.residue_confidence, + ) + + +def filter_alignment( + result: AlignedSequences, + threshold: float = 0.5, +) -> AlignedSequences: + """Remove low-confidence columns from an alignment result. + + Parameters + ---------- + result : AlignedSequences + Alignment result with confidence scores (from ensemble modes). + threshold : float + Columns with confidence below this value are removed entirely. + + Returns + ------- + AlignedSequences + New result with only high-confidence columns. Shorter sequences. + """ + if result.column_confidence is None: + import warnings + warnings.warn( + "No confidence scores available (requires ensemble mode). " + "Returning unfiltered alignment." + ) + return result + + conf = result.column_confidence + keep = [col for col in range(len(conf)) if conf[col] >= threshold] + + filtered_seqs = [] + for seq in result.sequences: + filtered_seqs.append(''.join(seq[col] for col in keep)) + + filtered_conf = [conf[col] for col in keep] + filtered_res_conf = None + if result.residue_confidence is not None: + filtered_res_conf = [ + [row[col] for col in keep] for row in result.residue_confidence + ] + + return AlignedSequences( + names=result.names, + sequences=filtered_seqs, + column_confidence=filtered_conf, + residue_confidence=filtered_res_conf, + ) + + +def write_confidence(path: str, result: AlignedSequences) -> None: + """Write per-column confidence scores to a text file. + + Parameters + ---------- + path : str + Output file path. + result : AlignedSequences + Alignment result with confidence scores. + """ + if result.column_confidence is None: + raise ValueError("No confidence scores available (requires ensemble mode)") + with open(path, 'w') as f: + for val in result.column_confidence: + f.write(f"{val:.4f}\n") + + __all__ = [ "align", "align_from_file", @@ -692,6 +812,9 @@ def align_file_to_file( "get_num_threads", "kalign", "AlignedSequences", + "mask_alignment", + "filter_alignment", + "write_confidence", "DNA", "DNA_INTERNAL", "RNA", diff --git a/scripts/bench_confidence_filtering.py b/scripts/bench_confidence_filtering.py new file mode 100644 index 0000000..ee97057 --- /dev/null +++ b/scripts/bench_confidence_filtering.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Benchmark: SP/TC on confident columns only. + +Scores BAliBASE alignments using only columns above a confidence threshold. +Shows that kalign's confidence scores identify reliable columns, and that +SP/TC on those columns is dramatically higher than on all columns. + +Usage: + uv run python scripts/bench_confidence_filtering.py +""" +import json +import os +import sys +import tempfile +import xml.etree.ElementTree as ET +from pathlib import Path + +import kalign + +BB_ROOT = Path(__file__).parent.parent / "benchmarks" / "data" / "downloads" / "bb3_release" + + +def parse_balibase_xml(xml_path): + tree = ET.parse(xml_path) + root = tree.getroot() + colsco = root.find(".//column-score/colsco-data") + if colsco is None or colsco.text is None: + return None + values = [int(v) for v in colsco.text.split()] + return [1 if v == 1 else 0 for v in values] + + +def discover_cases(): + cases = [] + for rv_dir in sorted(BB_ROOT.iterdir()): + if not rv_dir.is_dir() or not rv_dir.name.startswith("RV"): + continue + for tfa in sorted(rv_dir.glob("*.tfa")): + if tfa.stem.startswith("BBS"): + continue + msf = tfa.with_suffix(".msf") + xml = tfa.with_suffix(".xml") + if msf.exists(): + cases.append({ + "family": tfa.stem, + "category": rv_dir.name, + "unaligned": str(tfa), + "reference": str(msf), + "xml": str(xml) if xml.exists() else None, + }) + return cases + + +def score_with_xml(ref_path, test_path, xml_path): + """Score alignment using XML core block mask (standard BAliBASE scoring).""" + if xml_path and Path(xml_path).exists(): + mask = parse_balibase_xml(xml_path) + if mask: + return kalign.compare_detailed(ref_path, test_path, column_mask=mask) + return kalign.compare_detailed(ref_path, test_path, max_gap_frac=0.2) + + +def score_filtered(ref_path, test_sequences, test_names, conf, threshold, xml_path, tmpdir, family): + """Score only confident columns: mask out low-confidence residues, then score.""" + # Write a filtered test alignment: low-confidence residues become gaps + filtered_out = os.path.join(tmpdir, f"{family}_filt.fa") + with open(filtered_out, 'w') as f: + for name, seq in zip(test_names, test_sequences): + filtered = [] + for col in range(len(seq)): + if col < len(conf) and conf[col] >= threshold: + filtered.append(seq[col]) + else: + filtered.append('-') + f.write(f">{name}\n{''.join(filtered)}\n") + + # Score with XML mask (normal BAliBASE scoring on the filtered alignment) + return score_with_xml(ref_path, filtered_out, xml_path) + + +def main(): + if not BB_ROOT.exists(): + print(f"ERROR: BAliBASE not found at {BB_ROOT}") + return 1 + + cases = discover_cases() + thresholds = [0.0, 0.3, 0.5, 0.7, 0.9] + + print(f"{len(cases)} BAliBASE cases, accurate mode") + print() + + results_by_thresh = {t: {"recall": [], "precision": [], "f1": [], "tc": [], "n_cols": [], "n_total": []} + for t in thresholds} + + with tempfile.TemporaryDirectory() as tmpdir: + for i, case in enumerate(cases): + # Use align_from_file to get BOTH alignment and confidence in one call + aln = kalign.align_from_file(case["unaligned"], mode="accurate") + conf = aln.column_confidence + + # Write the alignment to file for scoring + out = os.path.join(tmpdir, f"{case['family']}.fa") + with open(out, 'w') as f: + for name, seq in zip(aln.names, aln.sequences): + f.write(f">{name}\n{seq}\n") + + for thresh in thresholds: + if thresh == 0.0: + # No confidence filtering — standard XML scoring + score = score_with_xml(case["reference"], out, case["xml"]) + else: + if conf is None: + continue + # Mask low-confidence residues to gaps, then score normally + score = score_filtered( + case["reference"], aln.sequences, aln.names, + conf, thresh, case["xml"], tmpdir, case["family"] + ) + + if score is not None: + for k in ["recall", "precision", "f1", "tc"]: + results_by_thresh[thresh][k].append(score[k]) + if conf is not None: + n_confident = sum(1 for c in conf if c >= thresh) + results_by_thresh[thresh]["n_cols"].append(n_confident) + results_by_thresh[thresh]["n_total"].append(len(conf)) + + if (i + 1) % 50 == 0: + print(f" {i+1}/{len(cases)}...") + + # Print results + print() + print(f"{'Threshold':>10s} {'Recall':>8s} {'Prec':>8s} {'F1':>8s} {'TC':>8s} {'Cols%':>7s} (n)") + print("=" * 60) + for thresh in thresholds: + r = results_by_thresh[thresh] + n = len(r["recall"]) + if n == 0: + continue + avg_r = sum(r["recall"]) / n + avg_p = sum(r["precision"]) / n + avg_f = sum(r["f1"]) / n + avg_t = sum(r["tc"]) / n + + if r["n_cols"] and r["n_total"]: + avg_pct = sum(c / t * 100 for c, t in zip(r["n_cols"], r["n_total"])) / len(r["n_cols"]) + else: + avg_pct = 100.0 + + label = "all" if thresh == 0.0 else f">={thresh}" + print(f"{label:>10s} {avg_r:8.3f} {avg_p:8.3f} {avg_f:8.3f} {avg_t:8.3f} {avg_pct:6.1f}% ({n})") + + # Also print comparison to other tools on all columns + print() + print("Reference (all columns, from manuscript):") + print(f"{'mafft':>10s} {'0.867':>8s} {'0.715':>8s} {'0.778':>8s} {'0.590':>8s}") + print(f"{'muscle':>10s} {'0.870':>8s} {'0.721':>8s} {'0.783':>8s} {'0.581':>8s}") + print(f"{'clustalo':>10s} {'0.840':>8s} {'0.710':>8s} {'0.764':>8s} {'0.559':>8s}") + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/src/parameters.c b/src/parameters.c index 2c8bf14..b31212a 100644 --- a/src/parameters.c +++ b/src/parameters.c @@ -60,6 +60,9 @@ struct parameters*init_param(void) param->rename = 0; param->clean = 0; param->unalign = 0; + param->confidence_threshold = 0.0f; + param->confidence_style = 0; /* KALIGN_MASK_LOWERCASE */ + param->confidence_output = NULL; return param; ERROR: free_parameters(param); diff --git a/src/parameters.h b/src/parameters.h index f4e301c..c13edea 100644 --- a/src/parameters.h +++ b/src/parameters.h @@ -29,6 +29,9 @@ struct parameters{ int min_support; char* load_poar; char* mode; /* "fast", "default", "recall", "accurate" (NULL = default) */ + float confidence_threshold; /* mask columns below this confidence (0=off) */ + int confidence_style; /* KALIGN_MASK_LOWERCASE or KALIGN_MASK_REMOVE */ + char* confidence_output; /* write per-column confidence to file (NULL=off) */ int help_flag; int quiet; int dump_internal; diff --git a/src/run_kalign.c b/src/run_kalign.c index 6d0ff5e..c0cba01 100644 --- a/src/run_kalign.c +++ b/src/run_kalign.c @@ -16,6 +16,9 @@ #define OPT_MODE 22 #define OPT_LOAD_POAR 20 #define OPT_MIN_SUPPORT 18 +#define OPT_CONF_THRESHOLD 30 +#define OPT_CONF_STYLE 31 +#define OPT_CONF_OUTPUT 32 static int set_aln_type(char* in, int* type); @@ -51,6 +54,10 @@ int print_kalign_help(char * argv[]) fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-n/--nthreads","Number of threads." ,"[auto]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--load-poar","Load POAR table for re-threshold." ,"[off]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--confidence-threshold","Mask columns below this confidence (0-1)." ,"[off]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--confidence-style","Masking style: lowercase or remove." ,"[lowercase]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--confidence-output","Write per-column confidence to file." ,"[off]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--version (-V/-v)","Prints version." ,"[NA]" ); fprintf(stdout,"\nExamples:\n\n"); @@ -128,6 +135,9 @@ int main(int argc, char *argv[]) {"load-poar", required_argument, 0, OPT_LOAD_POAR}, {"min-support", required_argument, 0, OPT_MIN_SUPPORT}, {"nthreads", required_argument, 0, 'n'}, + {"confidence-threshold", required_argument, 0, OPT_CONF_THRESHOLD}, + {"confidence-style", required_argument, 0, OPT_CONF_STYLE}, + {"confidence-output", required_argument, 0, OPT_CONF_OUTPUT}, {"input", required_argument, 0, 'i'}, {"infile", required_argument, 0, 'i'}, {"in", required_argument, 0, 'i'}, @@ -181,6 +191,19 @@ int main(int argc, char *argv[]) case OPT_MIN_SUPPORT: param->min_support = atoi(optarg); break; + case OPT_CONF_THRESHOLD: + param->confidence_threshold = (float)atof(optarg); + break; + case OPT_CONF_STYLE: + if(strcmp(optarg, "remove") == 0){ + param->confidence_style = KALIGN_MASK_REMOVE; + }else{ + param->confidence_style = KALIGN_MASK_LOWERCASE; + } + break; + case OPT_CONF_OUTPUT: + param->confidence_output = optarg; + break; case 'h': param->help_flag = 1; break; @@ -324,6 +347,17 @@ int run_kalign(struct parameters* param) RUN(kalign_align_full(msa, runs, n_runs, &ens, param->nthreads)); } + /* Apply confidence masking if requested */ + if(param->confidence_threshold > 0.0f){ + RUN(kalign_mask_by_confidence(msa, param->confidence_threshold, + param->confidence_style)); + } + + /* Write per-column confidence scores if requested */ + if(param->confidence_output != NULL){ + RUN(kalign_write_confidence(msa, param->confidence_output)); + } + RUN(kalign_write_msa(msa, param->outfile, param->format)); kalign_free_msa(msa); return OK; diff --git a/tests/python/test_confidence.py b/tests/python/test_confidence.py new file mode 100644 index 0000000..bda31e0 --- /dev/null +++ b/tests/python/test_confidence.py @@ -0,0 +1,122 @@ +"""Tests for confidence masking and filtering.""" +import pytest +import kalign +import os + +TEST_FILE = os.path.join( + os.path.dirname(__file__), "..", "data", "BB11001.tfa" +) + + +class TestConfidenceMasking: + """Test confidence masking with ensemble modes.""" + + def test_accurate_has_confidence(self): + """Accurate mode should produce confidence scores.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + assert result.column_confidence is not None + assert len(result.column_confidence) > 0 + assert all(0.0 <= c <= 1.0 for c in result.column_confidence) + + def test_fast_has_no_confidence(self): + """Fast mode (single run) should NOT produce confidence.""" + result = kalign.align_from_file(TEST_FILE, mode="fast") + assert result.column_confidence is None + + def test_mask_lowercase(self): + """mask_alignment with lowercase style.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + masked = kalign.mask_alignment(result, threshold=0.5, style="lowercase") + + # Masked sequences should have some lowercase chars + has_lower = any(c.islower() for seq in masked.sequences for c in seq if c != '-') + assert has_lower, "Expected some lowercase residues after masking" + + # Original should be unchanged (uppercase) + has_lower_orig = any(c.islower() for seq in result.sequences for c in seq if c != '-') + assert not has_lower_orig, "Original should not have lowercase" + + def test_mask_remove(self): + """mask_alignment with remove style replaces with gaps.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + masked = kalign.mask_alignment(result, threshold=0.5, style="remove") + + # Masked should have more gaps than original + orig_gaps = sum(seq.count('-') for seq in result.sequences) + masked_gaps = sum(seq.count('-') for seq in masked.sequences) + assert masked_gaps >= orig_gaps + + def test_mask_preserves_confident_residues(self): + """Confident columns should be unchanged after masking.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + masked = kalign.mask_alignment(result, threshold=0.5) + + conf = result.column_confidence + for i, seq in enumerate(result.sequences): + for col in range(len(seq)): + if col < len(conf) and conf[col] >= 0.5 and seq[col] != '-': + assert masked.sequences[i][col] == seq[col], \ + f"Confident residue changed at seq {i} col {col}" + + def test_mask_no_confidence_warns(self): + """mask_alignment on non-ensemble result should warn.""" + result = kalign.align_from_file(TEST_FILE, mode="fast") + with pytest.warns(UserWarning, match="No confidence"): + masked = kalign.mask_alignment(result, threshold=0.5) + # Should return unmodified + assert masked.sequences == result.sequences + + def test_filter_removes_columns(self): + """filter_alignment should remove low-confidence columns.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + filtered = kalign.filter_alignment(result, threshold=0.5) + + # Filtered should be shorter + assert len(filtered.sequences[0]) < len(result.sequences[0]) + + # All sequences should be same length + lengths = [len(s) for s in filtered.sequences] + assert len(set(lengths)) == 1 + + # Filtered confidence should all be >= threshold + assert all(c >= 0.5 for c in filtered.column_confidence) + + def test_filter_preserves_residue_count(self): + """Filtering should not create or destroy residues — only remove columns.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + filtered = kalign.filter_alignment(result, threshold=0.3) + + for i in range(len(result.sequences)): + orig_res = sum(1 for c in result.sequences[i] if c != '-') + filt_res = sum(1 for c in filtered.sequences[i] if c != '-') + # Filtered can have fewer residues (removed columns may have had residues) + assert filt_res <= orig_res + + def test_write_confidence(self, tmp_path): + """write_confidence should produce a file with one float per line.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + outfile = str(tmp_path / "conf.txt") + kalign.write_confidence(outfile, result) + + with open(outfile) as f: + lines = f.readlines() + + assert len(lines) == len(result.column_confidence) + for line in lines: + val = float(line.strip()) + assert 0.0 <= val <= 1.0 + + def test_threshold_zero_is_noop(self): + """Threshold 0 should not mask anything.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + masked = kalign.mask_alignment(result, threshold=0.0) + assert masked.sequences == result.sequences + + def test_threshold_one_masks_some(self): + """Threshold 1.0 should mask at least some columns.""" + result = kalign.align_from_file(TEST_FILE, mode="accurate") + masked = kalign.mask_alignment(result, threshold=1.0) + + # At least some residues should be lowercase + lower_count = sum(1 for seq in masked.sequences for c in seq if c.islower()) + assert lower_count > 0, "Expected some lowercase residues at threshold=1.0" From 53abded5272d6e27a574f1382fc87e7ccda6b370 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Fri, 27 Mar 2026 05:17:21 +0800 Subject: [PATCH 11/29] Add sequences to existing alignment (--add mode) New feature: align new sequences against an existing alignment without re-aligning the existing sequences. Builds a consensus profile from the existing alignment, aligns each new sequence via seq-to-profile Hirschberg DP, and inserts gaps to match the column structure. Strict mode (default): no new columns are introduced. Insertions in new sequences relative to the existing alignment are dropped. C library: - kalign_add_sequences(existing, new_seqs, n_threads) in aln_add.c - kalign_read_sequences() allows single-sequence input (for --add) - Consensus profile built from column frequencies + substitution scores CLI: - kalign --add new.fa --existing aligned.fa -o combined.fa - Both --add and --existing required together - Existing sequences preserved byte-identical in output Python API: - kalign.add_to_alignment(existing, new_seqs, output, format, n_threads) - _core.add_to_alignment_file() pybind11 binding Tests: 6 Python tests (basic add, existing unchanged, residue preservation, alignment length, file-not-found, larger dataset with 44 sequences). 15/15 C tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/CMakeLists.txt | 1 + lib/include/kalign/kalign.h | 6 + lib/src/aln_add.c | 377 +++++++++++++++++++++++++++++ lib/src/aln_add.h | 38 +++ lib/src/msa_io.c | 64 +++++ python-kalign/__init__.py | 38 +++ python-kalign/_core.cpp | 48 ++++ src/parameters.c | 2 + src/parameters.h | 2 + src/run_kalign.c | 82 +++++-- tests/python/test_add_sequences.py | 180 ++++++++++++++ 11 files changed, 823 insertions(+), 15 deletions(-) create mode 100644 lib/src/aln_add.c create mode 100644 lib/src/aln_add.h create mode 100644 tests/python/test_add_sequences.py diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index a18d8dc..406140d 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -39,6 +39,7 @@ set(source_files src/aln_wrap.c src/aln_apair_dist.c + src/aln_add.c src/aln_param.c src/aln_run.c diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index ee65e4e..7ec1074 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -52,6 +52,7 @@ struct msa; /* input output routines */ EXTERN int kalign_read_input(char* infile, struct msa** msa,int quiet); +EXTERN int kalign_read_sequences(char* infile, struct msa** msa, int quiet); EXTERN int kalign_write_msa(struct msa *msa, char *outfile, char *format); @@ -69,6 +70,11 @@ EXTERN int kalign_consensus_from_poar(struct msa* msa, const char* poar_path, int min_support); +/* Add sequences to existing alignment */ +EXTERN int kalign_add_sequences(struct msa* existing, + struct msa* new_seqs, + int n_threads); + /* Confidence masking */ #define KALIGN_MASK_LOWERCASE 0 #define KALIGN_MASK_REMOVE 1 diff --git a/lib/src/aln_add.c b/lib/src/aln_add.c new file mode 100644 index 0000000..0f535ae --- /dev/null +++ b/lib/src/aln_add.c @@ -0,0 +1,377 @@ +/* aln_add.c — Add new sequences to an existing alignment. + * + * Builds a consensus profile from the existing aligned sequences, then + * aligns each new sequence against it via seq-to-profile Hirschberg DP. + * The existing sequences are NOT modified — only gap characters are + * inserted into the new sequences to fit the existing column structure. + */ + +#include "tldevel.h" +#include +#include +#include + +#include "msa_struct.h" +#include "msa_alloc.h" +#include "msa_op.h" +#include "msa_check.h" +#include "alphabet.h" + +#include "aln_param.h" +#include "aln_struct.h" +#include "aln_mem.h" +#include "aln_setup.h" +#include "aln_controller.h" +#include "kalign/kalign.h" + +#ifdef USE_THREADPOOL +#include "threadpool/threadpool.h" +#endif + +#define ALN_ADD_IMPORT +#include "aln_add.h" + +/* Map a character (from finalized alignment) to internal index 0-22. + Returns -1 for gaps and unknown characters. + Uses the standard kalign protein alphabet: ARNDCQEGHILKMFPSTWYVBZX */ +static int char_to_internal(char c, int biotype) +{ + static const int protein_map[128] = { + /* 0-15 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + /* 16-31 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + /* 32-47 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + /* 48-63 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + /* @ABCDE */ -1, 0,20, 4, 3, 6,13, 7, 8, 9,-1,11,10,12, 2,-1, + /* PQRSTU */ 14, 5, 1,15,16,22,19,17,22,18,21,-1,-1,-1,-1,-1, + /* `abcde */ -1, 0,20, 4, 3, 6,13, 7, 8, 9,-1,11,10,12, 2,-1, + /* pqrstu */ 14, 5, 1,15,16,22,19,17,22,18,21,-1,-1,-1,-1,-1, + }; + static const int dna_map[128] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1, 0,-1, 1,-1,-1,-1, 2,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1, 3, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1, 0,-1, 1,-1,-1,-1, 2,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1, 3, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + }; + if(c < 0 || c == '-') return -1; + if(biotype == ALN_BIOTYPE_DNA){ + return dna_map[(unsigned char)c]; + } + return protein_map[(unsigned char)c]; +} + +/* Build a consensus profile from an existing finalized alignment. + Profile has alnlen columns in the standard 64-float-per-column format. + Residue frequencies are summed; substitution scores are weighted averages. */ +static int build_consensus_profile(struct msa* existing, + struct aln_param* ap, + float** prof_out) +{ + float* prof = NULL; + float** subm = ap->subm; + float gpo = ap->gpo; + float gpe = ap->gpe; + float tgpe = ap->tgpe; + int alnlen = existing->alnlen; + int numseq = existing->numseq; + int biotype = existing->biotype; + int i, j, col; + + MMALLOC(prof, sizeof(float) * (alnlen + 2) * 64); + + /* Point to trailing boundary row */ + prof += (64 * (alnlen + 1)); + + /* Trailing boundary */ + for(i = 0; i < 64; i++) prof[i] = 0; + prof[23 + 32] = -gpo; + prof[24 + 32] = -gpe; + prof[25 + 32] = -tgpe; + prof[55] = -gpo; + prof[56] = -gpe; + prof[57] = -tgpe; + + /* Fill each column from alnlen-1 down to 0 */ + for(col = alnlen - 1; col >= 0; col--){ + prof -= 64; + for(j = 0; j < 64; j++) prof[j] = 0; + + /* Count residue frequencies at this column */ + float freq[23]; + for(j = 0; j < 23; j++) freq[j] = 0.0f; + float total = 0.0f; + + for(i = 0; i < numseq; i++){ + char c = existing->sequences[i]->seq[col]; + int idx = char_to_internal(c, biotype); + if(idx >= 0 && idx < 23){ + freq[idx] += 1.0f; + total += 1.0f; + } + } + + /* Store frequency counts in positions 0-22 */ + for(j = 0; j < 23; j++){ + prof[j] = freq[j]; + } + + /* Compute substitution scores as weighted average over + observed residues: score[j] = sum(freq[c] * subm[c][j]) / total */ + prof += 32; + if(total > 0.0f){ + for(j = 0; j < 23; j++){ + float score = 0.0f; + for(int c = 0; c < 23; c++){ + if(freq[c] > 0.0f){ + score += freq[c] * subm[c][j]; + } + } + prof[j] = score / total; + } + } + /* Gap penalties */ + prof[23] = -gpo; + prof[24] = -gpe; + prof[25] = -tgpe; + prof -= 32; + + /* Store base penalties for set_gap_penalties_n */ + prof[55] = -gpo; + prof[56] = -gpe; + prof[57] = -tgpe; + } + + /* Leading boundary */ + prof -= 64; + for(i = 0; i < 64; i++) prof[i] = 0; + prof[23 + 32] = -gpo; + prof[24 + 32] = -gpe; + prof[25 + 32] = -tgpe; + prof[55] = -gpo; + prof[56] = -gpe; + prof[57] = -tgpe; + + *prof_out = prof; + return OK; +ERROR: + return FAIL; +} + +/* Align one new sequence against the consensus profile and produce + a gapped sequence string matching the existing alignment columns. + Returns a newly allocated string (caller must free). */ +static int align_one_to_profile(struct aln_param* ap, + float* cons_profile, + int profile_len, + int n_existing, + const uint8_t* new_seq, + int new_len, + char* new_seq_chars, + char** gapped_out) +{ + struct aln_mem* m = NULL; + char* gapped = NULL; + int i, c; + int pos_seq; + + RUN(alloc_aln_mem(&m, 256)); + m->ap = ap; + m->mode = ALN_MODE_FULL; + m->run_parallel = 0; + m->flip_threshold = 0.0F; + m->flip_trial = 0; + m->flip_stride = 1; + m->flip_counter = 0; + m->flip_mask = 0; + m->margin_sum = 0.0F; + m->margin_count = 0; + + /* Profile is always "seq1" (the longer axis in Hirschberg). + New sequence is "seq2". */ + m->len_a = profile_len; + m->len_b = new_len; + m->enda = profile_len; + m->endb = new_len; + + m->seq1 = NULL; /* not a sequence — it's a profile */ + m->seq2 = new_seq; + m->prof1 = cons_profile; + m->prof2 = NULL; + m->sip = n_existing; + m->consistency = NULL; + + m->f[0].a = 0.0F; + m->f[0].ga = -FLT_MAX; + m->f[0].gb = -FLT_MAX; + m->b[0].a = 0.0F; + m->b[0].ga = -FLT_MAX; + m->b[0].gb = -FLT_MAX; + + /* Scale gap penalties in profile by n_existing */ + RUN(set_gap_penalties_n(cons_profile, profile_len, n_existing)); + + RUN(init_alnmem(m)); + aln_runner(m); + RUN(add_gap_info_to_path_n(m)); + + /* Build gapped sequence from alignment path. + path[0] = alignment length + path[c]: 0=match, &1=gap in profile (insertion in new seq — SKIP in strict mode), + &2=gap in new seq (insert '-'), 3=end */ + MMALLOC(gapped, sizeof(char) * (m->path[0] + 2)); + + pos_seq = 0; + i = 0; + c = 1; + while(m->path[c] != 3){ + if(m->path[c] == 0){ + /* Match: new seq residue aligns to profile column */ + if(pos_seq < new_len){ + gapped[i] = new_seq_chars[pos_seq]; + }else{ + gapped[i] = '-'; + } + pos_seq++; + i++; + }else if(m->path[c] & 1){ + /* Gap in profile = insertion in new seq. + Strict mode: skip this residue (don't add new columns). */ + pos_seq++; + /* Don't increment i — residue is dropped */ + }else if(m->path[c] & 2){ + /* Gap in new seq: insert gap at this profile column */ + gapped[i] = '-'; + i++; + } + c++; + } + gapped[i] = '\0'; + + /* Verify: gapped length should equal profile_len (strict mode) */ + if(i != profile_len){ + /* May differ slightly — pad or truncate */ + while(i < profile_len){ + gapped[i] = '-'; + i++; + } + gapped[profile_len] = '\0'; + } + + free_aln_mem(m); + *gapped_out = gapped; + return OK; +ERROR: + if(m) free_aln_mem(m); + if(gapped) MFREE(gapped); + return FAIL; +} + +int kalign_add_sequences(struct msa* existing, + struct msa* new_seqs, + int n_threads) +{ + struct aln_param* ap = NULL; + float* cons_profile = NULL; + int i; + int numseq_existing; + int numseq_new; + int alnlen; + + ASSERT(existing != NULL, "No existing alignment"); + ASSERT(new_seqs != NULL, "No new sequences"); + /* Finalize if not already — ensures seq->seq has gap characters and alnlen is set */ + if(existing->aligned != ALN_STATUS_FINAL){ + if(existing->aligned == ALN_STATUS_ALIGNED){ + RUN(finalise_alignment(existing)); + }else{ + ERROR_MSG("Existing MSA must be aligned (status=%d)", existing->aligned); + } + } + + numseq_existing = existing->numseq; + numseq_new = new_seqs->numseq; + alnlen = existing->alnlen; + + if(numseq_new == 0){ + return OK; /* nothing to add */ + } + + /* Detect biotype if needed */ + if(existing->biotype == ALN_BIOTYPE_UNDEF){ + RUN(detect_alphabet(existing)); + } + if(new_seqs->biotype == ALN_BIOTYPE_UNDEF){ + new_seqs->biotype = existing->biotype; + } + + /* Encode new sequences to internal representation */ + if(existing->biotype == ALN_BIOTYPE_DNA){ + new_seqs->L = ALPHA_defDNA; + RUN(convert_msa_to_internal(new_seqs, ALPHA_defDNA)); + }else{ + new_seqs->L = ALPHA_ambigiousPROTEIN; + RUN(convert_msa_to_internal(new_seqs, ALPHA_ambigiousPROTEIN)); + } + + /* Init alignment parameters */ + RUN(aln_param_init(&ap, existing->biotype, n_threads, + KALIGN_MATRIX_AUTO, -1.0f, -1.0f, -1.0f)); + + /* Build consensus profile from existing alignment */ + RUN(build_consensus_profile(existing, ap, &cons_profile)); + + /* Align each new sequence to the consensus profile */ + for(i = 0; i < numseq_new; i++){ + char* gapped = NULL; + + RUN(align_one_to_profile( + ap, cons_profile, alnlen, numseq_existing, + new_seqs->sequences[i]->s, + new_seqs->sequences[i]->len, + new_seqs->sequences[i]->seq, + &gapped)); + + /* Replace the new sequence's seq with the gapped version */ + MFREE(new_seqs->sequences[i]->seq); + new_seqs->sequences[i]->seq = gapped; + } + + /* Append new sequences to existing MSA (manual, not merge_msa which + may change alignment properties). */ + { + int total = numseq_existing + numseq_new; + if(total > existing->alloc_numseq){ + MREALLOC(existing->sequences, + sizeof(struct msa_seq*) * total); + existing->alloc_numseq = total; + } + for(i = 0; i < numseq_new; i++){ + /* Transfer ownership of the new sequence from new_seqs to existing */ + existing->sequences[numseq_existing + i] = new_seqs->sequences[i]; + new_seqs->sequences[i] = NULL; + /* Update len to alnlen (gapped length) */ + existing->sequences[numseq_existing + i]->len = + (int)strlen(existing->sequences[numseq_existing + i]->seq); + } + existing->numseq = total; + } + existing->alnlen = alnlen; + existing->aligned = ALN_STATUS_FINAL; + + /* Cleanup */ + /* Free the profile — need to adjust pointer back to allocation start */ + { + float* prof_base = cons_profile; /* already points to start (leading boundary) */ + MFREE(prof_base); + } + aln_param_free(ap); + + return OK; +ERROR: + if(cons_profile) MFREE(cons_profile); + if(ap) aln_param_free(ap); + return FAIL; +} diff --git a/lib/src/aln_add.h b/lib/src/aln_add.h new file mode 100644 index 0000000..cbb8b5b --- /dev/null +++ b/lib/src/aln_add.h @@ -0,0 +1,38 @@ +#ifndef ALN_ADD_H +#define ALN_ADD_H + +#ifdef ALN_ADD_IMPORT +#define EXTERN +#else +#ifdef __cplusplus +#define EXTERN extern "C" +#else +#define EXTERN extern +#endif +#endif + +struct msa; + +/* Add new unaligned sequences to an existing finalized alignment. + * + * Each new sequence is independently aligned against a consensus profile + * built from the existing alignment. The existing sequences are NOT + * modified — their gaps are preserved exactly. + * + * After this call, existing->sequences contains the original sequences + * (unchanged) plus the new sequences (with gaps inserted). existing->numseq + * is updated accordingly. + * + * existing: Must be a finalized alignment (ALN_STATUS_FINAL). + * new_seqs: Unaligned sequences to add. Modified in place (seq->seq gets gaps). + * n_threads: Number of threads for parallel alignment of new sequences. + * + * Returns OK on success, FAIL on error. */ +EXTERN int kalign_add_sequences(struct msa* existing, + struct msa* new_seqs, + int n_threads); + +#undef ALN_ADD_IMPORT +#undef EXTERN + +#endif diff --git a/lib/src/msa_io.c b/lib/src/msa_io.c index a619bf4..8ffd908 100644 --- a/lib/src/msa_io.c +++ b/lib/src/msa_io.c @@ -173,6 +173,70 @@ int kalign_read_input(char* infile, struct msa** msa, int quiet) return FAIL; } +/* Like kalign_read_input but allows 1 sequence (for --add mode). */ +int kalign_read_sequences(char* infile, struct msa** msa, int quiet) +{ + /* Read normally but without the >= 2 check */ + struct msa* backup = *msa; + *msa = NULL; + + /* Use the same read logic but skip check_for_sequences. + We replicate the core of kalign_read_input here. */ + struct in_buffer* b = NULL; + struct msa* m = NULL; + int type; + int i, j; + + if(infile && !my_file_exists(infile)){ + ERROR_MSG("File: %s does not exist.", infile); + } + + RUN(read_file_stdin(&b, infile)); + j = 0; + for(i = 0; i < MACRO_MIN(1, b->n_lines); i++){ + j += b->l[i]->len - 1; + } + if(j == 0){ + free_in_buffer(b); + *msa = backup; + return OK; + } + + RUN(detect_alignment_format(b, &type)); + if(type == FORMAT_FA){ + RUN(read_fasta(b, &m)); + }else if(type == FORMAT_MSF){ + RUN(read_msf(b, &m)); + }else if(type == FORMAT_CLU){ + RUN(read_clu(b, &m)); + }else{ + free_in_buffer(b); + ERROR_MSG("Could not detect input format in: %s", infile ? infile : "stdin"); + } + m->quiet = quiet; + RUN(detect_alphabet(m)); + RUN(detect_aligned(m)); + RUN(set_sip_nsip(m)); + free_in_buffer(b); + + if(m->numseq < 1){ + ERROR_MSG("No sequences found in %s", infile ? infile : "stdin"); + } + + if(backup != NULL){ + RUN(merge_msa(&backup, m)); + kalign_free_msa(m); + *msa = backup; + }else{ + *msa = m; + } + return OK; +ERROR: + if(m) kalign_free_msa(m); + *msa = backup; + return FAIL; +} + int check_for_sequences(struct msa* msa) { if(!msa){ diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index 17f7296..566768b 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -783,6 +783,43 @@ def filter_alignment( ) +def add_to_alignment( + existing: str, + new_sequences: str, + output: str, + format: str = "fasta", + n_threads: Optional[int] = None, +) -> None: + """Add new sequences to an existing alignment. + + Each new sequence is aligned against the consensus profile of the + existing alignment. The existing sequences are NOT re-aligned — their + gaps are preserved exactly. + + Parameters + ---------- + existing : str + Path to existing alignment file (FASTA/MSF/Clustal). + new_sequences : str + Path to file with new unaligned sequences. + output : str + Path to output file (existing + new sequences, all aligned). + format : str, optional + Output format: "fasta", "msf", "clu" (default: "fasta"). + n_threads : int, optional + Number of threads. + """ + if not os.path.exists(existing): + raise FileNotFoundError(f"Existing alignment not found: {existing}") + if not os.path.exists(new_sequences): + raise FileNotFoundError(f"New sequences file not found: {new_sequences}") + + if n_threads is None: + n_threads = get_num_threads() + + _core.add_to_alignment_file(existing, new_sequences, output, format, n_threads) + + def write_confidence(path: str, result: AlignedSequences) -> None: """Write per-column confidence scores to a text file. @@ -815,6 +852,7 @@ def write_confidence(path: str, result: AlignedSequences) -> None: "mask_alignment", "filter_alignment", "write_confidence", + "add_to_alignment", "DNA", "DNA_INTERNAL", "RNA", diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index 715b172..e36f54f 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -17,6 +17,7 @@ extern "C" { #include "msa_alloc.h" #include "msa_op.h" #include "msa_cmp.h" + #include "aln_add.h" #include "dssim.h" } @@ -832,6 +833,53 @@ PYBIND11_MODULE(_core, m) { py::arg("terminal_gap_extend") = -1.0f, "Alias for align_file_to_file(). Align file to file using a named mode preset."); + m.def("add_to_alignment_file", []( + const std::string& existing_file, + const std::string& new_seqs_file, + const std::string& output_file, + const std::string& format, + int n_threads + ) { + struct msa* existing = nullptr; + struct msa* new_seqs = nullptr; + + int result = kalign_read_input( + const_cast(existing_file.c_str()), &existing, 1); + if (result != 0 || !existing) { + throw std::runtime_error("Failed to read existing alignment: " + existing_file); + } + + result = kalign_read_sequences( + const_cast(new_seqs_file.c_str()), &new_seqs, 1); + if (result != 0 || !new_seqs) { + kalign_free_msa(existing); + throw std::runtime_error("Failed to read new sequences: " + new_seqs_file); + } + + result = kalign_add_sequences(existing, new_seqs, n_threads); + if (result != 0) { + kalign_free_msa(existing); + kalign_free_msa(new_seqs); + throw std::runtime_error("Failed to add sequences to alignment"); + } + + kalign_free_msa(new_seqs); + + result = kalign_write_msa(existing, + const_cast(output_file.c_str()), + const_cast(format.c_str())); + kalign_free_msa(existing); + if (result != 0) { + throw std::runtime_error("Failed to write output alignment"); + } + }, + py::arg("existing_file"), + py::arg("new_seqs_file"), + py::arg("output_file"), + py::arg("format") = "fasta", + py::arg("n_threads") = 1, + "Add new sequences to an existing alignment without re-aligning existing sequences."); + // Matrix constants (canonical names) m.attr("MATRIX_AUTO") = KALIGN_MATRIX_AUTO; m.attr("MATRIX_PFASUM43") = KALIGN_MATRIX_PFASUM43; diff --git a/src/parameters.c b/src/parameters.c index b31212a..41171c4 100644 --- a/src/parameters.c +++ b/src/parameters.c @@ -60,6 +60,8 @@ struct parameters*init_param(void) param->rename = 0; param->clean = 0; param->unalign = 0; + param->add_file = NULL; + param->existing_file = NULL; param->confidence_threshold = 0.0f; param->confidence_style = 0; /* KALIGN_MASK_LOWERCASE */ param->confidence_output = NULL; diff --git a/src/parameters.h b/src/parameters.h index c13edea..516ed94 100644 --- a/src/parameters.h +++ b/src/parameters.h @@ -29,6 +29,8 @@ struct parameters{ int min_support; char* load_poar; char* mode; /* "fast", "default", "recall", "accurate" (NULL = default) */ + char* add_file; /* new sequences to add to existing alignment */ + char* existing_file; /* existing alignment to add sequences to */ float confidence_threshold; /* mask columns below this confidence (0=off) */ int confidence_style; /* KALIGN_MASK_LOWERCASE or KALIGN_MASK_REMOVE */ char* confidence_output; /* write per-column confidence to file (NULL=off) */ diff --git a/src/run_kalign.c b/src/run_kalign.c index c0cba01..918545b 100644 --- a/src/run_kalign.c +++ b/src/run_kalign.c @@ -1,6 +1,8 @@ #include "tldevel.h" #include "tlmisc.h" #include "kalign/kalign.h" +#include "msa_struct.h" +#include "aln_add.h" #include "parameters.h" #include @@ -19,6 +21,8 @@ #define OPT_CONF_THRESHOLD 30 #define OPT_CONF_STYLE 31 #define OPT_CONF_OUTPUT 32 +#define OPT_ADD 33 +#define OPT_EXISTING 34 static int set_aln_type(char* in, int* type); @@ -54,6 +58,11 @@ int print_kalign_help(char * argv[]) fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"-n/--nthreads","Number of threads." ,"[auto]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--load-poar","Load POAR table for re-threshold." ,"[off]"); + fprintf(stdout,"\n"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--add FILE","New sequences to add to existing alignment." ,"[off]"); + fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--existing FILE","Existing alignment (new seqs added to this)." ,"[off]"); + + fprintf(stdout,"\n"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--confidence-threshold","Mask columns below this confidence (0-1)." ,"[off]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--confidence-style","Masking style: lowercase or remove." ,"[lowercase]"); fprintf(stdout,"%*s%-*s: %s %s\n",3,"",MESSAGE_MARGIN-3,"--confidence-output","Write per-column confidence to file." ,"[off]"); @@ -138,6 +147,8 @@ int main(int argc, char *argv[]) {"confidence-threshold", required_argument, 0, OPT_CONF_THRESHOLD}, {"confidence-style", required_argument, 0, OPT_CONF_STYLE}, {"confidence-output", required_argument, 0, OPT_CONF_OUTPUT}, + {"add", required_argument, 0, OPT_ADD}, + {"existing", required_argument, 0, OPT_EXISTING}, {"input", required_argument, 0, 'i'}, {"infile", required_argument, 0, 'i'}, {"in", required_argument, 0, 'i'}, @@ -204,6 +215,12 @@ int main(int argc, char *argv[]) case OPT_CONF_OUTPUT: param->confidence_output = optarg; break; + case OPT_ADD: + param->add_file = optarg; + break; + case OPT_EXISTING: + param->existing_file = optarg; + break; case 'h': param->help_flag = 1; break; @@ -267,29 +284,39 @@ int main(int argc, char *argv[]) param->num_infiles += argc-optind; } - if(param->num_infiles == 0){ + /* --add mode doesn't need -i input files */ + if(param->add_file != NULL && param->existing_file != NULL){ + /* Validate both files exist */ + param->num_infiles = 0; /* not using normal input */ + }else if(param->add_file != NULL || param->existing_file != NULL){ + LOG_MSG("Both --add and --existing must be specified together."); + free_parameters(param); + return EXIT_FAILURE; + }else if(param->num_infiles == 0){ RUN(print_kalign_help(argv)); LOG_MSG("No input files"); free_parameters(param); return EXIT_SUCCESS; } - MMALLOC(param->infile, sizeof(char*) * param->num_infiles); + if(param->num_infiles > 0){ + MMALLOC(param->infile, sizeof(char*) * param->num_infiles); - c = 0; - if(in){ - param->infile[c] = (strcmp(in, "-") == 0) ? NULL : in; - c++; - } + c = 0; + if(in){ + param->infile[c] = (strcmp(in, "-") == 0) ? NULL : in; + c++; + } - if (optind < argc){ - while (optind < argc){ - if(strcmp(argv[optind], "-") == 0){ - param->infile[c] = NULL; /* stdin */ - }else{ - param->infile[c] = argv[optind]; + if (optind < argc){ + while (optind < argc){ + if(strcmp(argv[optind], "-") == 0){ + param->infile[c] = NULL; /* stdin */ + }else{ + param->infile[c] = argv[optind]; + } + c++; + optind++; } - c++; - optind++; } } @@ -312,6 +339,31 @@ int run_kalign(struct parameters* param) struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); int n_runs = 0; + /* --add mode: add new sequences to existing alignment */ + if(param->add_file != NULL && param->existing_file != NULL){ + struct msa* new_seqs = NULL; + + /* Read existing alignment */ + RUN(kalign_read_input(param->existing_file, &msa, param->quiet)); + + /* Read new sequences (allows single sequence) */ + RUN(kalign_read_sequences(param->add_file, &new_seqs, param->quiet)); + + if(!param->quiet){ + LOG_MSG("Adding %d sequences to existing alignment of %d sequences", + new_seqs->numseq, msa->numseq); + } + + /* Add new sequences to existing alignment */ + RUN(kalign_add_sequences(msa, new_seqs, param->nthreads)); + kalign_free_msa(new_seqs); + + /* Write result */ + RUN(kalign_write_msa(msa, param->outfile, param->format)); + kalign_free_msa(msa); + return OK; + } + if(param->num_infiles == 1){ RUN(kalign_read_input(param->infile[0], &msa, param->quiet)); }else{ diff --git a/tests/python/test_add_sequences.py b/tests/python/test_add_sequences.py new file mode 100644 index 0000000..57218ab --- /dev/null +++ b/tests/python/test_add_sequences.py @@ -0,0 +1,180 @@ +"""Tests for adding sequences to an existing alignment.""" +import pytest +import kalign +import os +import tempfile + +TEST_FILE = os.path.join( + os.path.dirname(__file__), "..", "data", "BB11001.tfa" +) + + +def _read_fasta(path): + """Read a FASTA file, return list of (name, sequence) tuples.""" + entries = [] + name = None + seq_parts = [] + with open(path) as f: + for line in f: + if line.startswith('>'): + if name: + entries.append((name, ''.join(seq_parts))) + name = line.strip()[1:] + seq_parts = [] + else: + seq_parts.append(line.strip()) + if name: + entries.append((name, ''.join(seq_parts))) + return entries + + +def _make_existing_and_new(test_file, n_holdout=1): + """Align all sequences, then split into existing alignment + held-out sequences.""" + result = kalign.align_from_file(test_file, mode="fast") + + # Write existing alignment (all but last n_holdout sequences) + existing_path = tempfile.mktemp(suffix=".fa") + with open(existing_path, 'w') as f: + for name, seq in zip(result.names[:-n_holdout], result.sequences[:-n_holdout]): + f.write(f">{name}\n{seq}\n") + + # Write held-out sequences (unaligned, from original file) + names_seqs = [] + with open(test_file) as f: + name = None + seq_parts = [] + for line in f: + if line.startswith('>'): + if name: + names_seqs.append((name, ''.join(seq_parts))) + name = line.strip()[1:] + seq_parts = [] + else: + seq_parts.append(line.strip()) + if name: + names_seqs.append((name, ''.join(seq_parts))) + + new_path = tempfile.mktemp(suffix=".fa") + with open(new_path, 'w') as f: + for name, seq in names_seqs[-n_holdout:]: + f.write(f">{name}\n{seq}\n") + + return existing_path, new_path, result + + +class TestAddSequences: + + def test_basic_add(self): + """Basic add: 3 existing + 1 new = 4 sequences.""" + existing_path, new_path, full_result = _make_existing_and_new(TEST_FILE, 1) + out_path = tempfile.mktemp(suffix=".fa") + + kalign.add_to_alignment(existing_path, new_path, out_path) + + result = _read_fasta(out_path) + assert len(result) == len(full_result.sequences) + + # All sequences should have the same length + lengths = set(len(seq) for _, seq in result) + assert len(lengths) == 1, f"Unequal lengths: {lengths}" + + os.remove(existing_path) + os.remove(new_path) + os.remove(out_path) + + def test_existing_unchanged(self): + """Existing sequences must not be modified (content, ignoring line wrapping).""" + existing_path, new_path, _ = _make_existing_and_new(TEST_FILE, 1) + out_path = tempfile.mktemp(suffix=".fa") + + existing_seqs = _read_fasta(existing_path) + + kalign.add_to_alignment(existing_path, new_path, out_path) + + output_seqs = _read_fasta(out_path) + + for i in range(len(existing_seqs)): + assert existing_seqs[i][1] == output_seqs[i][1], \ + f"Existing sequence {i} was modified!\n before: {existing_seqs[i][1][:60]}...\n after: {output_seqs[i][1][:60]}..." + + os.remove(existing_path) + os.remove(new_path) + os.remove(out_path) + + def test_residue_preservation(self): + """Added sequences must preserve all residues (no residues lost).""" + existing_path, new_path, _ = _make_existing_and_new(TEST_FILE, 1) + out_path = tempfile.mktemp(suffix=".fa") + + # Read original new sequence + with open(new_path) as f: + lines = f.readlines() + orig_seq = ''.join(l.strip() for l in lines if not l.startswith('>')) + orig_residues = len(orig_seq) + + kalign.add_to_alignment(existing_path, new_path, out_path) + + # Read last sequence from output (the added one) + result = _read_fasta(out_path) + added_seq = result[-1][1] + added_residues = sum(1 for c in added_seq if c != '-') + + assert added_residues == orig_residues, \ + f"Residue count changed: {orig_residues} -> {added_residues}" + + os.remove(existing_path) + os.remove(new_path) + os.remove(out_path) + + def test_alignment_length_matches(self): + """Output alignment length must match existing alignment length.""" + existing_path, new_path, _ = _make_existing_and_new(TEST_FILE, 1) + out_path = tempfile.mktemp(suffix=".fa") + + existing_seqs = _read_fasta(existing_path) + existing_alnlen = len(existing_seqs[0][1]) + + kalign.add_to_alignment(existing_path, new_path, out_path) + + output_seqs = _read_fasta(out_path) + output_alnlen = len(output_seqs[0][1]) + + assert output_alnlen == existing_alnlen, \ + f"Alignment length changed: {existing_alnlen} -> {output_alnlen}" + + os.remove(existing_path) + os.remove(new_path) + os.remove(out_path) + + def test_file_not_found(self): + """Should raise FileNotFoundError for missing files.""" + with pytest.raises(FileNotFoundError): + kalign.add_to_alignment("/nonexistent/file.fa", TEST_FILE, "/tmp/out.fa") + with pytest.raises(FileNotFoundError): + kalign.add_to_alignment(TEST_FILE, "/nonexistent/file.fa", "/tmp/out.fa") + + def test_larger_dataset(self): + """Test with BB30014 (44 sequences, hold out 5).""" + test_file = os.path.join( + os.path.dirname(__file__), "..", "data", "BB30014.tfa" + ) + if not os.path.exists(test_file): + pytest.skip("BB30014.tfa not found") + + existing_path, new_path, full_result = _make_existing_and_new(test_file, 5) + out_path = tempfile.mktemp(suffix=".fa") + + kalign.add_to_alignment(existing_path, new_path, out_path) + + result = _read_fasta(out_path) + + # Should have all 44 sequences + assert len(result) == len(full_result.sequences) + + # All same length + lengths = set(len(seq) for _, seq in result) + assert len(lengths) == 1 + + os.remove(existing_path) + os.remove(new_path) + os.remove(out_path) From 280a40e927a3acf2aff6084190839f3e610ef028 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Thu, 2 Apr 2026 05:44:37 +0800 Subject: [PATCH 12/29] Fix _mm_malloc guards, update build.zig for zig 0.15, modernize benchmark runner - Add missing #include with HAVE_AVX2 guards in aln_apair_dist.c and aln_wrap.c (fixes undefined symbol on GCC 10) - Update build.zig to zig 0.15 API (addLibrary/createModule), add aln_add.c - Rewrite benchmark runner: replace --refine with --mode (fast/default/recall/accurate), simplify work dispatch, add per-category SP/Prec/F1/TC summary tables - Containerfile: clone from git instead of COPY, no external binary dependencies - Containerfile.downstream: same git-based approach Co-Authored-By: Claude Opus 4.6 (1M context) --- Containerfile | 65 ++++++++---------- Containerfile.downstream | 125 ++++++++++++++++----------------- benchmarks/runner.py | 145 +++++++++++++++++++-------------------- build.zig | 63 ++++++++--------- lib/src/aln_apair_dist.c | 5 ++ lib/src/aln_wrap.c | 5 ++ 6 files changed, 193 insertions(+), 215 deletions(-) diff --git a/Containerfile b/Containerfile index 478b40b..d84de66 100644 --- a/Containerfile +++ b/Containerfile @@ -3,25 +3,23 @@ # Includes kalign, Clustal Omega, MAFFT, and MUSCLE v5 for comparative # benchmarking on BAliBASE, BRAliBASE, and BaliFam100 datasets. # -# Build: +# Build (installs kalign from the 'extra' branch): # podman build -t kalign-benchmark . # -# Run the interactive dashboard: -# podman run -it -p 8050:8050 \ -# -v ./benchmarks/data:/kalign/benchmarks/data \ -# kalign-benchmark +# Build from a specific commit for reproducibility: +# podman build --build-arg KALIGN_REF=abc1234 -t kalign-benchmark . # -# Run a CLI benchmark: +# Run benchmarks: # podman run -it \ -# -v ./benchmarks/data:/kalign/benchmarks/data \ +# -v ./benchmarks/data:/data \ # kalign-benchmark \ # python -m benchmarks \ -# --dataset balibase --method python_api clustalo mafft muscle -v +# --dataset balibase --method cli clustalo mafft muscle \ +# --mode fast default recall accurate -v # -# View results in the dashboard after a CLI run: +# Run the interactive dashboard: # podman run -it -p 8050:8050 \ -# -v ./benchmarks/data:/kalign/benchmarks/data \ -# -v ./benchmarks/results:/kalign/benchmarks/results \ +# -v ./benchmarks/data:/data \ # kalign-benchmark FROM ubuntu:24.04 @@ -37,7 +35,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # ---------- Build MUSCLE v5 from source ---------- -# myutils.h checks __arm64__ (macOS) but not __aarch64__ (Linux); add it RUN cd /tmp && \ git clone --depth 1 https://github.com/rcedgar/muscle.git && \ cd muscle/src && \ @@ -46,41 +43,33 @@ RUN cd /tmp && \ cp ../bin/muscle /usr/local/bin/ && \ rm -rf /tmp/muscle -# ---------- Copy kalign source and build ---------- -COPY . /kalign +# ---------- Python environment ---------- +RUN python3 -m venv /venv +ENV PATH="/venv/bin:$PATH" +RUN pip install --no-cache-dir uv + +# ---------- Clone kalign and build ---------- +ARG KALIGN_REF=extra +RUN git clone --branch ${KALIGN_REF} --depth 1 \ + https://github.com/TimoLassmann/kalign.git /kalign WORKDIR /kalign -# Build kalign twice: threadpool and OpenMP -RUN mkdir cbuild-tp && cd cbuild-tp && \ +# Build kalign C binary (threadpool, no OpenMP) +RUN mkdir cbuild && cd cbuild && \ cmake -DCMAKE_BUILD_TYPE=Release -DUSE_OPENMP=OFF -DUSE_THREADPOOL=ON .. && \ - make -j"$(nproc)" - -RUN mkdir cbuild-omp && cd cbuild-omp && \ - cmake -DCMAKE_BUILD_TYPE=Release -DUSE_OPENMP=ON -DUSE_THREADPOOL=OFF .. && \ - make -j"$(nproc)" + make -j"$(nproc)" && \ + cp src/kalign /usr/local/bin/kalign -# ---------- Python environment ---------- -RUN python3 -m venv /venv -ENV PATH="/venv/bin:/kalign/cbuild-tp/src:$PATH" - -RUN pip install --no-cache-dir uv && \ - uv pip install --no-cache -e ".[benchmark]" \ +# Install Python package with benchmark dependencies +RUN uv pip install --no-cache -e ".[benchmark]" \ --config-settings='cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON' -# ---------- Verify tools ---------- -RUN which kalign && which clustalo && which mafft && which muscle +# ---------- Verify all tools ---------- +RUN kalign --version && clustalo --version && mafft --version && muscle -version -# ---------- Data & results directories ---------- +# ---------- Data directory (mount point for BAliBASE etc.) ---------- RUN mkdir -p /kalign/benchmarks/data/downloads /kalign/benchmarks/results -# ---------- Hot-swap: cross-compiled kalign binary (last for fast rebuilds) ---------- -COPY zig-out/kalign-linux-aarch64 /usr/local/bin/kalign -RUN chmod +x /usr/local/bin/kalign - -# Rebuild Python module with latest source (uses cached venv layer) -RUN uv pip install --no-cache -e ".[benchmark]" \ - --config-settings='cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON' - EXPOSE 8050 CMD ["python", "-m", "benchmarks.app", "--host", "0.0.0.0", "--port", "8050"] diff --git a/Containerfile.downstream b/Containerfile.downstream index e144ed8..5081a31 100644 --- a/Containerfile.downstream +++ b/Containerfile.downstream @@ -1,32 +1,26 @@ -# Kalign Downstream Benchmark Container +# Kalign Paper Benchmark Container # -# Extends the base benchmark setup with tools for downstream application -# benchmarks: positive selection (HyPhy), phylogenetics (IQ-TREE), -# homology detection (HMMER), and confidence comparison (GUIDANCE2). +# Single container with ALL tools at pinned versions for reproducible +# paper benchmarks. Includes alignment tools (kalign, ClustalO, MAFFT, +# MUSCLE), downstream tools (INDELible, HyPhy, IQ-TREE, HMMER, GUIDANCE2), +# and the full Python environment for running the manuscript pipeline. # # Build: -# podman build -f Containerfile.downstream -t kalign-downstream . +# podman build -f Containerfile.downstream -t kalign-paper . # -# Run all downstream benchmarks: -# podman run -it \ -# -v ./benchmarks/data:/kalign/benchmarks/data \ -# -v ./benchmarks/results:/kalign/benchmarks/results \ -# kalign-downstream \ -# python -m benchmarks.downstream --all -j 4 +# Run the manuscript pipeline: +# podman run --rm -it \ +# -v /path/to/2026_kalign_35:/manuscript \ +# -w /manuscript \ +# kalign-paper \ +# bash -c "uv pip install -e . && snakemake --cores 16" # -# Quick smoke test (5 cases per pipeline): -# podman run -it \ +# Run a single benchmark: +# podman run --rm -it \ # -v ./benchmarks/data:/kalign/benchmarks/data \ # -v ./benchmarks/results:/kalign/benchmarks/results \ -# kalign-downstream \ -# python -m benchmarks.downstream --all -j 4 --quick -# -# Generate figures from existing results: -# podman run -it \ -# -v ./benchmarks/results:/kalign/benchmarks/results \ -# -v ./benchmarks/figures:/kalign/benchmarks/figures \ -# kalign-downstream \ -# python -m benchmarks.downstream --figures -o benchmarks/figures/ +# kalign-paper \ +# python -m benchmarks.bench_quality_timing --threads 16 FROM ubuntu:24.04 @@ -37,24 +31,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential cmake g++ git curl wget ca-certificates \ python3 python3-pip python3-venv python3-dev \ pkg-config zlib1g-dev libcurl4-openssl-dev libssl-dev libeigen3-dev libboost-dev \ - clustalo mafft hmmer \ + hmmer \ perl libwww-perl libbio-perl-perl cpanminus \ && rm -rf /var/lib/apt/lists/* -# Bio::Perl convenience module (removed from BioPerl core in 1.7.x) RUN cpanm --notest Bio::Perl -# ── MUSCLE v5 from source ─────────────────────────────────────────── -# myutils.h checks __arm64__ (macOS) but not __aarch64__ (Linux); add it +# ── MAFFT (latest from source) ─────────────────────────────────────── RUN cd /tmp && \ - git clone --depth 1 https://github.com/rcedgar/muscle.git && \ - cd muscle/src && \ - sed -i 's/defined(__arm64__)/defined(__arm64__) || defined(__aarch64__)/' myutils.h && \ - bash build_linux.bash && \ - cp ../bin/muscle /usr/local/bin/ && \ - rm -rf /tmp/muscle - -# ── INDELible v1.03 from source ───────────────────────────────────── + wget -q https://mafft.cbrc.jp/alignment/software/mafft-7.526-without-extensions-src.tgz && \ + tar xzf mafft-7.526-without-extensions-src.tgz && \ + cd mafft-7.526-without-extensions/core && \ + make clean && make -j"$(nproc)" && make install && \ + rm -rf /tmp/mafft-* + +# ── ClustalO (latest from source) ──────────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + libargtable2-dev && rm -rf /var/lib/apt/lists/* && \ + cd /tmp && \ + wget -q http://www.clustal.org/omega/clustal-omega-1.2.4.tar.gz && \ + tar xzf clustal-omega-1.2.4.tar.gz && \ + cd clustal-omega-1.2.4 && \ + ./configure && make -j"$(nproc)" && make install && \ + rm -rf /tmp/clustal-omega-* + +# ── MUSCLE v5.3 (prebuilt binary) ──────────────────────────────────── +RUN wget -q https://github.com/rcedgar/muscle/releases/download/v5.3/muscle-linux-x86.v5.3 \ + -O /usr/local/bin/muscle && \ + chmod +x /usr/local/bin/muscle + +# ── INDELible v1.03 from source ────────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 https://github.com/matsengrp/indelible.git && \ cd indelible/src && \ @@ -62,7 +68,7 @@ RUN cd /tmp && \ cp indelible /usr/local/bin/ && \ rm -rf /tmp/indelible -# ── HyPhy from source ─────────────────────────────────────────────── +# ── HyPhy from source ──────────────────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 https://github.com/veg/hyphy.git && \ cd hyphy && \ @@ -74,7 +80,7 @@ RUN cd /tmp && \ ENV HYPHY_LIB=/usr/local/lib/hyphy ENV HYPHY_PATH=/usr/local/lib/hyphy -# ── IQ-TREE 2 from source ─────────────────────────────────────────── +# ── IQ-TREE 2 from source ──────────────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 --recurse-submodules https://github.com/iqtree/iqtree2.git && \ cd iqtree2 && \ @@ -84,11 +90,7 @@ RUN cd /tmp && \ cp iqtree2 /usr/local/bin/ && \ rm -rf /tmp/iqtree2 -# ── GUIDANCE2 from GitHub (original tar.gz URL is dead) ──────────── -# guidance.pl uses FindBin-relative paths ($Bin, $Bin/../Selecton, etc.) -# We install the full www/ tree under /opt/guidance-root/ so sibling -# directories (Selecton, bioSequence_scripts_and_constants) resolve -# correctly relative to the guidance.pl script location. +# ── GUIDANCE2 from GitHub ──────────────────────────────────────────── RUN cd /tmp && \ git clone --depth 1 https://github.com/anzaika/guidance.git && \ cd guidance && make && \ @@ -112,49 +114,40 @@ RUN cd /tmp && \ chmod +x /usr/local/bin/guidance2 && \ rm -rf /tmp/guidance -# ── Kalign C build ────────────────────────────────────────────────── +# ── Kalign (threadpool build) ───────────────────────────────────────── COPY . /kalign WORKDIR /kalign -RUN mkdir -p build && cd build && \ - cmake -DCMAKE_BUILD_TYPE=Release .. && \ +RUN mkdir cbuild && cd cbuild && \ + cmake -DCMAKE_BUILD_TYPE=Release -DUSE_OPENMP=OFF -DUSE_THREADPOOL=ON .. && \ make -j"$(nproc)" -# ── Record tool versions at build time ────────────────────────────── +# ── Record tool versions at build time ──────────────────────────────── RUN echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /tool_versions.txt && \ - echo "kalign=$(build/src/kalign --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ + echo "kalign=$(cbuild/src/kalign --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "mafft=$(mafft --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ - echo "muscle=$(muscle --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ + echo "muscle=$(muscle -version 2>&1 || echo unknown)" >> /tool_versions.txt && \ echo "clustalo=$(clustalo --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "hmmer=$(hmmbuild -h 2>&1 | grep '^# HMMER' | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "iqtree=$(iqtree2 --version 2>&1 | grep 'IQ-TREE' | head -1 || echo unknown)" >> /tool_versions.txt && \ echo "indelible=1.03" >> /tool_versions.txt && \ - echo "hyphy=$(hyphy --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt + echo "hyphy=$(hyphy --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ + cat /tool_versions.txt -# ── Python environment ────────────────────────────────────────────── +# ── Python environment ──────────────────────────────────────────────── RUN python3 -m venv /venv -ENV PATH="/venv/bin:/kalign/build/src:$PATH" +ENV PATH="/venv/bin:/kalign/cbuild/src:$PATH" RUN pip install --no-cache-dir uv && \ - uv pip install --no-cache -e ".[benchmark]" && \ + uv pip install --no-cache -e ".[benchmark]" \ + --config-settings='cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON' && \ uv pip install --no-cache \ - dendropy biopython pandas matplotlib scipy seaborn numpy + dendropy biopython pandas matplotlib scipy seaborn numpy snakemake -# ── Verify tools ──────────────────────────────────────────────────── +# ── Verify tools ────────────────────────────────────────────────────── RUN which kalign && which clustalo && which mafft && which muscle && \ which hmmbuild && which hmmsearch && which iqtree2 && \ which hyphy && which indelible && which guidance2 -# ── Data & results directories ────────────────────────────────────── -RUN mkdir -p benchmarks/data/downloads/pfam_seed \ - benchmarks/data/downloads/swissprot \ - benchmarks/data/downloads/selectome \ - benchmarks/results/calibration \ - benchmarks/results/positive_selection \ - benchmarks/results/phylo_accuracy \ - benchmarks/results/hmmer_detection \ - benchmarks/figures - -EXPOSE 8050 - -CMD ["python", "-m", "benchmarks.downstream", "--help"] +# ── Data directories ────────────────────────────────────────────────── +RUN mkdir -p benchmarks/data/downloads benchmarks/results diff --git a/benchmarks/runner.py b/benchmarks/runner.py index ad3cb9a..a9526dc 100644 --- a/benchmarks/runner.py +++ b/benchmarks/runner.py @@ -14,40 +14,33 @@ def _run_one(args): """Worker function for parallel execution.""" - case, method, binary, n_threads, refine, adaptive_budget, ensemble = args + case, method, binary, n_threads, mode = args return run_case(case, method=method, binary=binary, n_threads=n_threads, - refine=refine, adaptive_budget=adaptive_budget, ensemble=ensemble) + mode=mode) def _result_label(r) -> str: - """Format a concise label showing method and config for verbose output.""" + """Format a concise label for verbose output.""" if r.method in EXTERNAL_TOOLS: return r.method - parts = ["kalign"] - if r.refine != "none": - parts.append(f"refine={r.refine}") - if r.ensemble: - parts.append(f"ens={r.ensemble}") - return " ".join(parts) + return f"kalign {r.refine}" def run_benchmark( dataset: str = "balibase", methods: Optional[List[str]] = None, - refine_modes: Optional[List[str]] = None, + modes: Optional[List[str]] = None, max_cases: int = 0, binary: str = "kalign", n_threads: int = 1, verbose: bool = False, - adaptive_budget: bool = False, - ensemble: int = 0, parallel: int = 1, ) -> List[AlignmentResult]: """Run benchmark suite and return results.""" if methods is None: - methods = ["python_api"] - if refine_modes is None: - refine_modes = ["none"] + methods = ["cli"] + if modes is None: + modes = ["default"] cases = get_cases(dataset, max_cases=max_cases if max_cases > 0 else None) @@ -56,9 +49,11 @@ def run_benchmark( print("Try running with --download-only first.") return [] - print(f"Running {len(cases)} cases from '{dataset}' with methods: {methods}, refine: {refine_modes}") + print(f"Running {len(cases)} cases from '{dataset}'") + print(f" Methods: {methods}") + print(f" Modes: {modes}") if parallel > 1: - print(f"Using {parallel} parallel workers") + print(f" Workers: {parallel}") print() # Build work items @@ -66,16 +61,14 @@ def run_benchmark( for case in cases: for method in methods: if method in EXTERNAL_TOOLS: - # External tools don't support refine/ensemble — run once - work.append((case, method, binary, n_threads, "none", False, 0)) + work.append((case, method, binary, n_threads, "default")) else: - for refine in refine_modes: - work.append((case, method, binary, n_threads, refine, adaptive_budget, ensemble)) + for mode in modes: + work.append((case, method, binary, n_threads, mode)) total = len(work) if parallel <= 1: - # Sequential (original behavior) results = [] for i, item in enumerate(work): result = _run_one(item) @@ -87,15 +80,15 @@ def run_benchmark( else: print(f"[{i+1}/{total}] {result.family:<12} {label:<25} SP={result.recall:.3f} TC={result.tc:.3f} F1={result.f1:.3f} {result.wall_time:.1f}s") else: - # Parallel execution - results = [None] * total + results = [] done = 0 with ProcessPoolExecutor(max_workers=parallel) as pool: futures = {pool.submit(_run_one, item): i for i, item in enumerate(work)} + indexed_results = [None] * total for future in as_completed(futures): idx = futures[future] result = future.result() - results[idx] = result + indexed_results[idx] = result done += 1 if verbose: label = _result_label(result) @@ -103,6 +96,7 @@ def run_benchmark( print(f"[{done}/{total}] {result.family:<12} {label:<25} ERROR: {result.error}") else: print(f"[{done}/{total}] {result.family:<12} {label:<25} SP={result.recall:.3f} TC={result.tc:.3f} F1={result.f1:.3f} {result.wall_time:.1f}s") + results = [r for r in indexed_results if r is not None] return results @@ -113,36 +107,54 @@ def print_summary(results: List[AlignmentResult]) -> None: for r in results: if r.error: continue - ens = f" ensemble={r.ensemble}" if r.ensemble else "" - key = f"{r.method} refine={r.refine}{ens}" + if r.method in EXTERNAL_TOOLS: + key = r.method + else: + key = f"kalign {r.refine}" by_group.setdefault(key, []).append(r) + print(f"\n{'Method':<24} {'SP':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time':>8} {'N':>5}") + print("-" * 75) + for group, group_results in sorted(by_group.items()): recalls = [r.recall for r in group_results] precisions = [r.precision for r in group_results] f1s = [r.f1 for r in group_results] tcs = [r.tc for r in group_results] - times = [r.wall_time for r in group_results] - - print(f"\n--- {group} ({len(group_results)} cases) ---") - print(f" SP: mean={statistics.mean(recalls):.3f} " - f"median={statistics.median(recalls):.3f} " - f"min={min(recalls):.3f} max={max(recalls):.3f}") - print(f" TC: mean={statistics.mean(tcs):.3f} " - f"median={statistics.median(tcs):.3f}") - print(f" Precision: mean={statistics.mean(precisions):.3f} " - f"median={statistics.median(precisions):.3f}") - print(f" F1: mean={statistics.mean(f1s):.3f} " - f"median={statistics.median(f1s):.3f}") - print(f" Time (s): total={sum(times):.1f} " - f"mean={statistics.mean(times):.2f} " - f"max={max(times):.2f}") + total_time = sum(r.wall_time for r in group_results) + + print(f"{group:<24} {statistics.mean(recalls):>8.3f} {statistics.mean(precisions):>8.3f} " + f"{statistics.mean(f1s):>8.3f} {statistics.mean(tcs):>8.3f} " + f"{total_time:>7.0f}s {len(group_results):>5}") + + # Per-category breakdown + categories = sorted({r.dataset for r in results if not r.error}) + if len(categories) > 1: + for cat in categories: + cat_results = [r for r in results if r.dataset == cat and not r.error] + if not cat_results: + continue + cat_groups = {} + for r in cat_results: + key = r.method if r.method in EXTERNAL_TOOLS else f"kalign {r.refine}" + cat_groups.setdefault(key, []).append(r) + + cat_name = cat.replace("balibase_", "") + n = len(next(iter(cat_groups.values()))) + print(f"\n--- {cat_name} ({n} cases) ---") + print(f"{'Method':<24} {'SP':>8} {'Prec':>8} {'F1':>8} {'TC':>8}") + print("-" * 60) + for group, gr in sorted(cat_groups.items()): + print(f"{group:<24} {statistics.mean(r.recall for r in gr):>8.3f} " + f"{statistics.mean(r.precision for r in gr):>8.3f} " + f"{statistics.mean(r.f1 for r in gr):>8.3f} " + f"{statistics.mean(r.tc for r in gr):>8.3f}") errors = [r for r in results if r.error] if errors: print(f"\n{len(errors)} error(s):") for r in errors: - print(f" {r.family} ({r.method} refine={r.refine}): {r.error}") + print(f" {r.family} ({r.method}): {r.error}") def save_results(results: List[AlignmentResult], path: str) -> None: @@ -157,22 +169,16 @@ def save_results(results: List[AlignmentResult], path: str) -> None: for r in results: if r.error: continue - ens = f"_ensemble={r.ensemble}" if r.ensemble else "" - key = f"{r.method}_refine={r.refine}{ens}" + key = r.method if r.method in EXTERNAL_TOOLS else f"kalign_{r.refine}" by_group.setdefault(key, []).append(r) for group, group_results in by_group.items(): - scores = [r.sp_score for r in group_results] recalls = [r.recall for r in group_results] precisions = [r.precision for r in group_results] f1s = [r.f1 for r in group_results] tcs = [r.tc for r in group_results] data["summary"][group] = { "n_cases": len(group_results), - "sp_mean": statistics.mean(scores), - "sp_median": statistics.median(scores), - "sp_min": min(scores), - "sp_max": max(scores), "recall_mean": statistics.mean(recalls), "precision_mean": statistics.mean(precisions), "f1_mean": statistics.mean(f1s), @@ -200,9 +206,16 @@ def main() -> None: parser.add_argument( "--method", nargs="+", - default=["python_api"], + default=["cli"], choices=["python_api", "cli", "clustalo", "mafft", "muscle"], - help="Alignment method(s) to test (default: python_api)", + help="Alignment method(s) to test (default: cli)", + ) + parser.add_argument( + "--mode", + nargs="+", + default=["default"], + choices=["fast", "default", "recall", "accurate"], + help="Kalign mode preset(s) to test (default: default)", ) parser.add_argument( "--max-cases", @@ -212,15 +225,8 @@ def main() -> None: ) parser.add_argument( "--binary", - default="build/src/kalign", - help="Path to C-compiled kalign binary for CLI method (default: build/src/kalign)", - ) - parser.add_argument( - "--refine", - nargs="+", - default=["none"], - choices=["none", "all", "confident"], - help="Refinement mode(s) to test (default: none)", + default="kalign", + help="Path to kalign binary for CLI method (default: kalign)", ) parser.add_argument( "--threads", @@ -233,22 +239,11 @@ def main() -> None: default="", help="Output JSON file for results", ) - parser.add_argument( - "--adaptive-budget", - action="store_true", - help="Scale trial count by uncertainty", - ) - parser.add_argument( - "--ensemble", - type=int, - default=0, - help="Number of ensemble runs (0 = off)", - ) parser.add_argument( "-j", "--parallel", type=int, default=1, - help="Number of parallel workers for benchmark cases (default: 1)", + help="Number of parallel workers (default: 1)", ) parser.add_argument( "--download-only", @@ -271,13 +266,11 @@ def main() -> None: results = run_benchmark( dataset=args.dataset, methods=args.method, - refine_modes=args.refine, + modes=args.mode, max_cases=args.max_cases, binary=args.binary, n_threads=args.threads, verbose=args.verbose, - adaptive_budget=args.adaptive_budget, - ensemble=args.ensemble, parallel=args.parallel, ) diff --git a/build.zig b/build.zig index de7b607..7409923 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); -const ArrayList = std.ArrayList; const kalignPackageVersion = "3.5.1"; @@ -9,44 +7,54 @@ const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .aarch64, .os_tag = .linux }, .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu }, .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl }, - // .{ .cpu_arch = .x86_64, .os_tag = .windows }, }; const cflags = [_][]const u8{ - "-DKALIGN_PACKAGE_VERSION=\"3.5.0\"", + "-DKALIGN_PACKAGE_VERSION=\"" ++ kalignPackageVersion ++ "\"", "-DKALIGN_PACKAGE_NAME=\"kalign\"", "-DKALIGN_ALN_SERIAL_THRESHOLD=250", "-DKALIGN_KMEANS_UPGMA_THRESHOLD=50", }; pub fn build(b: *std.Build) !void { - // const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); for (targets) |t| { - const lib = b.addStaticLibrary(.{ - .name = "tldevel", + // --- Static library module --- + const lib_mod = b.createModule(.{ .target = b.resolveTargetQuery(t), .optimize = optimize, + .link_libc = true, + }); + lib_mod.addIncludePath(b.path("lib/src")); + lib_mod.addIncludePath(b.path("lib/include")); + lib_mod.addCSourceFiles(.{ .files = &kalign_lib_sources, .flags = &cflags }); + + const lib = b.addLibrary(.{ + .name = "tldevel", + .linkage = .static, + .root_module = lib_mod, }); - lib.linkLibC(); - lib.addIncludePath(.{ .path = "./lib/src" }); - lib.addCSourceFiles(.{ .files = &kalign_lib_sources, .flags = &cflags }); - lib.addIncludePath(.{ .path = "./lib/include" }); b.installArtifact(lib); - const kalign_bin = b.addExecutable(.{ - .name = "kalign", + // --- Executable module --- + const bin_mod = b.createModule(.{ .target = b.resolveTargetQuery(t), .optimize = optimize, + .link_libc = true, }); + bin_mod.addIncludePath(b.path("lib/src")); + bin_mod.addIncludePath(b.path("lib/include")); + bin_mod.addCSourceFiles(.{ .files = &kalign_sources, .flags = &cflags }); + bin_mod.linkLibrary(lib); - lib.addCSourceFiles(.{ .files = &kalign_sources, .flags = &cflags }); - kalign_bin.addIncludePath(.{ .path = "./lib/src" }); - kalign_bin.addIncludePath(.{ .path = "./lib/include" }); - kalign_bin.linkLibrary(lib); + const kalign_bin = b.addExecutable(.{ + .name = "kalign", + .root_module = bin_mod, + }); b.installArtifact(kalign_bin); + // Install into a per-target subdirectory (e.g. zig-out/x86_64-linux-gnu/kalign) const target_output = b.addInstallArtifact(kalign_bin, .{ .dest_dir = .{ .override = .{ @@ -57,25 +65,9 @@ pub fn build(b: *std.Build) !void { b.getInstallStep().dependOn(&target_output.step); } - - // const exe = b.addExecutable(.{ - // .name = "zig_test", - // .target = target, - // .optimize = optimize, - // }); - // exe.addCSourceFile(.{ .file = .{ .path = "./tests/zig_test.c" }, .flags = &[_][]const u8{"-std=c99"} }); - - // // exe.addCSourceFiles(.{ .files = &kalign_lib_sources, .flags = &[_][]const u8{"-std=c99"} }); - // // exe.addCSourceFile(&.{"./tests/zig_test.c"}, cflags.items); - // // exe.addCSourceFile("./lib/src/strnlen_compat.c", cflags.items); - // exe.addIncludePath(.{ .path = "./lib/src" }); - // // exe.linkLibrary(lib); - // exe.linkLibC(); - // b.installArtifact(exe); } const kalign_lib_sources = [_][]const u8{ - // "lib/src/strnlen_compat.c", "lib/src/test.c", "lib/src/tldevel.c", "lib/src/tlmisc.c", @@ -97,6 +89,7 @@ const kalign_lib_sources = [_][]const u8{ "lib/src/pick_anchor.c", "lib/src/aln_wrap.c", "lib/src/aln_apair_dist.c", + "lib/src/aln_add.c", "lib/src/aln_param.c", "lib/src/aln_run.c", "lib/src/aln_mem.c", @@ -116,6 +109,6 @@ const kalign_lib_sources = [_][]const u8{ }; const kalign_sources = [_][]const u8{ - "./src/run_kalign.c", - "./src/parameters.c", + "src/run_kalign.c", + "src/parameters.c", }; diff --git a/lib/src/aln_apair_dist.c b/lib/src/aln_apair_dist.c index 5d05997..5924c59 100644 --- a/lib/src/aln_apair_dist.c +++ b/lib/src/aln_apair_dist.c @@ -1,6 +1,11 @@ #include "tldevel.h" #include "msa_struct.h" +#ifdef HAVE_AVX2 +#include +#include +#endif + #ifdef USE_THREADPOOL #include "threadpool.h" #endif diff --git a/lib/src/aln_wrap.c b/lib/src/aln_wrap.c index 9f99db6..93ac65a 100644 --- a/lib/src/aln_wrap.c +++ b/lib/src/aln_wrap.c @@ -4,6 +4,11 @@ #include "esl_stopwatch.h" #include "task.h" #include "msa_struct.h" + +#ifdef HAVE_AVX2 +#include +#include +#endif #include "msa_op.h" #include "msa_alloc.h" #include "msa_check.h" From 79f325bb2bfabaac23f39c6b736d298dc8f74f18 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Fri, 17 Apr 2026 18:18:55 +0800 Subject: [PATCH 13/29] Fix benchmark corruption bugs: temp-file race, length-invariant check, defensive buffer init align_from_file_mode in _core.cpp wrote alignments to a fixed path /tmp/kalign_output.fa and read them back. Parallel benchmark subprocesses contending for this file produced corrupted output (wrong sequence counts, wrong names, embedded null bytes). Replaced with direct reads from the msa struct. finalise_alignment now asserts that every sequence's len + sum(gaps) matches the aln_len computed from sequence 0, converting silent gap-invariant violations into clear errors. Also memsets the linear buffer to '-' so any unwritten positions stay as gap characters rather than uninitialized memory on glibc. Same defensive init applied to kalign_msa_to_arr and the strict-mode gapped buffer in aln_add, where partial population is plausible. msa_seq_cpy fix: copy gaps[src->len] instead of gaps[src->alloc_len]. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/src/aln_add.c | 2 ++ lib/src/msa_op.c | 27 ++++++++++++++--------- lib/src/msa_op.h | 2 +- python-kalign/_core.cpp | 47 +++++++++-------------------------------- 4 files changed, 30 insertions(+), 48 deletions(-) diff --git a/lib/src/aln_add.c b/lib/src/aln_add.c index 0f535ae..e9b96b7 100644 --- a/lib/src/aln_add.c +++ b/lib/src/aln_add.c @@ -222,6 +222,8 @@ static int align_one_to_profile(struct aln_param* ap, path[c]: 0=match, &1=gap in profile (insertion in new seq — SKIP in strict mode), &2=gap in new seq (insert '-'), 3=end */ MMALLOC(gapped, sizeof(char) * (m->path[0] + 2)); + memset(gapped, '-', m->path[0] + 1); + gapped[m->path[0] + 1] = '\0'; pos_seq = 0; i = 0; diff --git a/lib/src/msa_op.c b/lib/src/msa_op.c index feec768..fb2ec95 100644 --- a/lib/src/msa_op.c +++ b/lib/src/msa_op.c @@ -3,6 +3,7 @@ #include "alphabet.h" #include +#include #include "msa_alloc.h" #define MSA_OP_IMPORT @@ -67,7 +68,7 @@ int msa_seq_cpy(struct msa_seq *d, struct msa_seq *src) d->s[j] = src->s[j]; d->gaps[j] = src->gaps[j]; } - d->gaps[src->alloc_len] = src->gaps[src->alloc_len]; + d->gaps[src->len] = src->gaps[src->len]; d->seq[src->len] = 0; d->len = src->len; d->rank = src->rank; @@ -386,6 +387,8 @@ int kalign_msa_to_arr(struct msa* msa, char ***aligned, int *out_aln_len) for(int i = 0 ; i < numseq;i++){ out[i] = NULL; MMALLOC(out[i], sizeof(char) * (aln_len +1)); + memset(out[i], '-', aln_len); + out[i][aln_len] = 0; } /* galloc(&out, numseq,aln_len+1); */ @@ -550,21 +553,28 @@ static int aln_unknown_warning_message_same_len_no_gaps(void) int finalise_alignment(struct msa* msa) { - ASSERT(msa->aligned == ALN_STATUS_ALIGNED, "Sequences are not aligned"); struct msa_seq* seq = NULL; char* linear_seq = NULL; int aln_len = 0; + ASSERT(msa->aligned == ALN_STATUS_ALIGNED, "Sequences are not aligned"); - for(int i = 0; i <= msa->sequences[0]->len;i++){ aln_len += msa->sequences[0]->gaps[i]; } aln_len += msa->sequences[0]->len; for(int i = 0; i < msa->numseq;i++){ + int seq_aln_len = 0; MMALLOC(linear_seq, sizeof(char)* (aln_len+1)); + memset(linear_seq, '-', aln_len); + linear_seq[aln_len] = 0; seq = msa->sequences[i]; - RUN(make_linear_sequence(seq,linear_seq)); + RUN(make_linear_sequence(seq, linear_seq, &seq_aln_len)); + if(seq_aln_len != aln_len){ + ERROR_MSG("Alignment length mismatch: seq %d (%s) " + "has length %d, expected %d", + i, seq->name, seq_aln_len, aln_len); + } MFREE(seq->seq); seq->seq = linear_seq; /* seq->len = aln_len; */ @@ -574,31 +584,28 @@ int finalise_alignment(struct msa* msa) msa->aligned = ALN_STATUS_FINAL; return OK; ERROR: + if(linear_seq) MFREE(linear_seq); return FAIL; } -int make_linear_sequence(struct msa_seq* seq, char* linear_seq) +int make_linear_sequence(struct msa_seq* seq, char* linear_seq, int* out_len) { int c,j,f; f = 0; for(j = 0;j < seq->len;j++){ - //LOG_MSG("%d %d",j,seq->gaps[j]); for(c = 0;c < seq->gaps[j];c++){ linear_seq[f] = '-'; f++; - } - //LOG_MSG("%d %d %d",j,f,seq->gaps[j]); linear_seq[f] = seq->seq[j]; f++; } for(c = 0;c < seq->gaps[ seq->len];c++){ - //LOG_MSG("%d %d",j,seq->gaps[seq->len]); linear_seq[f] = '-'; f++; } linear_seq[f] = 0; - ///fprintf(stdout,"LINEAR:%s\n",linear_seq); + if(out_len) *out_len = f; return OK; } diff --git a/lib/src/msa_op.h b/lib/src/msa_op.h index f0a1d92..97bd60e 100644 --- a/lib/src/msa_op.h +++ b/lib/src/msa_op.h @@ -30,7 +30,7 @@ EXTERN int kalign_msa_to_arr(struct msa *msa, char ***aligned, int *out_aln_len) EXTERN int kalign_arr_to_msa(char **input_sequences, int *len, int numseq, struct msa **multiple_aln); EXTERN int finalise_alignment(struct msa* msa); -EXTERN int make_linear_sequence(struct msa_seq *seq, char *linear_seq); +EXTERN int make_linear_sequence(struct msa_seq *seq, char *linear_seq, int *out_len); /* Confidence masking styles */ #define KALIGN_MASK_LOWERCASE 0 diff --git a/python-kalign/_core.cpp b/python-kalign/_core.cpp index e36f54f..ad2a46d 100644 --- a/python-kalign/_core.cpp +++ b/python-kalign/_core.cpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include // Include DSSim for sequence simulation and MSA structures @@ -486,45 +484,20 @@ py::object align_from_file_mode( py::object confidence = extract_confidence(msa_data, msa_data->numseq); - /* Write to temp file to get gap-inserted FASTA output */ - const char* tmpdir = std::getenv("TMPDIR"); - if (!tmpdir) tmpdir = std::getenv("TMP"); - if (!tmpdir) tmpdir = std::getenv("TEMP"); - if (!tmpdir) tmpdir = "/tmp"; - std::string temp_file = std::string(tmpdir) + "/kalign_output.fa"; - result = kalign_write_msa(msa_data, const_cast(temp_file.c_str()), - const_cast("fasta")); - kalign_free_msa(msa_data); - - if (result != 0) { - throw std::runtime_error("Failed to write alignment results"); - } + int numseq = msa_data->numseq; + int alnlen = msa_data->alnlen; - std::ifstream file(temp_file); std::vector names; std::vector aligned_sequences; - std::string line, current_name, current_seq; - - while (std::getline(file, line)) { - if (line.empty()) continue; - if (line[0] == '>') { - if (!current_seq.empty()) { - names.push_back(current_name); - aligned_sequences.push_back(current_seq); - current_seq.clear(); - } - current_name = line.substr(1); - auto ws = current_name.find_first_of(" \t"); - if (ws != std::string::npos) current_name = current_name.substr(0, ws); - } else { - current_seq += line; - } - } - if (!current_seq.empty()) { - names.push_back(current_name); - aligned_sequences.push_back(current_seq); + names.reserve(numseq); + aligned_sequences.reserve(numseq); + + for (int i = 0; i < numseq; i++) { + names.emplace_back(msa_data->sequences[i]->name); + aligned_sequences.emplace_back(msa_data->sequences[i]->seq, alnlen); } - std::remove(temp_file.c_str()); + + kalign_free_msa(msa_data); if (!confidence.is_none()) { return py::make_tuple(names, aligned_sequences, confidence); From 67308a1343f4da02243e24c211b2b37970906643 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 06:43:01 +0800 Subject: [PATCH 14/29] Bump locked dependencies to resolve Dependabot CVEs Refreshes uv.lock via `uv lock --upgrade`. All flagged transitive deps (cryptography, flask, werkzeug, pillow, pygments, pytest, urllib3, requests, mako, black) now resolve to versions past their CVE fixes. pip-audit reports no known vulnerabilities. Test suite unaffected. --- uv.lock | 3534 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 2067 insertions(+), 1467 deletions(-) diff --git a/uv.lock b/uv.lock index d9d5859..4f302a5 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -12,7 +15,8 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] [[package]] @@ -29,7 +33,8 @@ name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } wheels = [ @@ -41,9 +46,12 @@ name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -62,7 +70,8 @@ name = "alembic" version = "1.16.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "mako", marker = "python_full_version < '3.10'" }, @@ -80,9 +89,12 @@ name = "alembic" version = "1.18.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -120,7 +132,8 @@ name = "array-api-compat" version = "1.11.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/6c/1e/d04312a19a67744298b7546885149488b8afbb965dfe693aa4964bb60586/array_api_compat-1.11.2.tar.gz", hash = "sha256:a3b7f7b6af18f4c42e79423b1b2479798998b6a74355069d77a01a5282755b5d", size = 50776, upload-time = "2025-03-20T13:11:55.095Z" } wheels = [ @@ -129,12 +142,15 @@ wheels = [ [[package]] name = "array-api-compat" -version = "1.13.0" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -143,9 +159,47 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/68/36/f799b36d7025a92a23819f9f06541babdb84b6fd0bd4253f8be2eca348a4/array_api_compat-1.13.0.tar.gz", hash = "sha256:8b83a56aa8b9477472fee37f7731968dd213e20c198a05ac49caeff9b03f48a6", size = 103065, upload-time = "2025-12-28T11:26:57.734Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/5d/493b1b5528ab5072feae30821ff3a07b7a0474213d548efb1fdf135f85c1/array_api_compat-1.13.0-py3-none-any.whl", hash = "sha256:c15026a0ddec42815383f07da285472e1b1ff2e632eb7afbcfe9b08fcbad9bf1", size = 58585, upload-time = "2025-12-28T11:26:56.081Z" }, +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 = "ast-serialize" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/1f/50f241d4e01fe75f4bba6a209edd4047c4b26acf70992ff885fd161f79cb/ast_serialize-0.4.0.tar.gz", hash = "sha256:74e4e634ab82d1466acf0be27043178570b98ebeaa3165f9240a6fad4c286471", size = 60687, upload-time = "2026-05-14T22:44:38.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/85/232631c59b5ca7152c08f026e9a46f47d852298acff74edd04a1fc1d0005/ast_serialize-0.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a6f26937ce0293aafbece0e39019e020369a5a70486ff4088227f0cc888844a9", size = 1182685, upload-time = "2026-05-14T22:43:40.205Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/4838d4d3ddc4425555601467d4e2a565e4340899e45feee4e32c80fbc911/ast_serialize-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:074032142777e3e6091977dc3c5146a8ca58ae6825b7f64e9a0b604153ddabd8", size = 1173113, upload-time = "2026-05-14T22:43:41.937Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/d622b19fc1c79a62028ec17f4ad4323177af25b174d32b07c84d61ef9d47/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:404f3462b4532e13a70b8849bba241dbd82e30043ff58d98c7e762fd925b116a", size = 1234117, upload-time = "2026-05-14T22:43:43.977Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b5/72f8c8659da0b64562e6d97f852d5c2022c74577df27c922e1e7065039ce/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97c55336e16f5c4ca2bde7be94cca4b8f7d665d64f7008925a82e02707ba14ac", size = 1231703, upload-time = "2026-05-14T22:43:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/7b/98/ccc51ee4f90f97a1ed0a0848bd4c9d77a80969849db8a262b7d2970a6a15/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:732b4ef76adcb0f298a7d18c4558336d83b1384f9ae0c7eaa1dc8d031b0a4390", size = 1441574, upload-time = "2026-05-14T22:43:47.784Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ce/668c4efe79e09c9cc97a4d0a1c29e61fe6f78857fe1e57c086772af55f89/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3db87c4772097c0782250bcd550d66b1189a8c889793c7bcf153f4fee70005c", size = 1254040, upload-time = "2026-05-14T22:43:49.879Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/38b27bc2909b7236939801ca9f0d97cdc6198da4f435a81658e0db506fdb/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43729a5e369ebbe7750635c0c206bc616fcd36e703cb9c4497d6b4df0291ee64", size = 1257847, upload-time = "2026-05-14T22:43:51.607Z" }, + { url = "https://files.pythonhosted.org/packages/68/df/360ebccc361235c167a8be2a0476870cb9ef44c42413bf1289b885684052/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:91d3786f3929786cdc4eeedfd110abb4603e7f6c1390c5af398f333a947b742d", size = 1298683, upload-time = "2026-05-14T22:43:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/7d5e0b4d47aafa1600c19e3670f962f81a9bf3da1bc25a1382529a447cf3/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7fba7315fd4bd87cb5560792709f6e66e0606402d362c0a38dd32dfb66ba6066", size = 1409438, upload-time = "2026-05-14T22:43:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/8875b2f1af3ec1539b88ff193dfbfa5573084ef7fcab27ea4cd09b6dc829/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4db9769d57deb5545ce56ebbbbe3436dcc0ae2688ce14c295cd14e106624ece7", size = 1507922, upload-time = "2026-05-14T22:43:56.959Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/5ec6927eb493ece7ba64263cdc556be889e0c62a013b1851bbe674a0dcda/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:dcd04f85a29deb80400e8987cfaceb9907140f763453cbffdbd6ff36f1b32c12", size = 1502817, upload-time = "2026-05-14T22:43:59.081Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c8/40cb818a08396b1f34d6189c0c42aec917dd331e11fb7c3b870cc61b795a/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:905fc11940831454d93589bd7ce2acb6a5eb01c2936156f751d2a21087c98cd3", size = 1454318, upload-time = "2026-05-14T22:44:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/d51494b60cc52f4792be5ddc951631cddb17a2990154634549abdbdbb5bf/ast_serialize-0.4.0-cp314-cp314t-win32.whl", hash = "sha256:3bdde2c4570143791f636aed4e3ef868f5b46eb90a18f8d5c41dd045aab08bef", size = 1060098, upload-time = "2026-05-14T22:44:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c9/b0086257c79ff95743a3621448a01fc71b234ae359d3d54cda383aa43939/ast_serialize-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6551d55b8607b97a7755683d743200b398c61a0b71a11b7f00c89c335a11d0f4", size = 1101015, upload-time = "2026-05-14T22:44:05.055Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6d/3dfddef4990fda47745af6615a3e51c4de711eda56c3a8072a0d8b6181c7/ast_serialize-0.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7234ff086cb152ea2a3b7ef895b5ebeb6d80779df049d5c6431c8e3536d5b03c", size = 1074495, upload-time = "2026-05-14T22:44:07.186Z" }, + { url = "https://files.pythonhosted.org/packages/be/d5/044c5f995ef75807a0effb56fc288cfdedeeb571222450fb6f7d94fd52f1/ast_serialize-0.4.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dcded5056d9f3d201df7833082c07ebcbc566ffc3d4105c9fc9fe278fa086ecb", size = 1189800, upload-time = "2026-05-14T22:44:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5a/52163557789d59a8197c10912ab4a1791c9143731ba0e3d9283ac0791db6/ast_serialize-0.4.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bd50d201098aae0d202805fe9606c0545492f69a3ec4403337e32c54ad29fc41", size = 1181713, upload-time = "2026-05-14T22:44:11.286Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c3/678ce3b6cb594b01c361da87f6c5679d26c1dae1583a082a8cd190e7232e/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6615b39cd747967c3aabe68bf3f5f26748e823cc6b474ddc1510ed188a824149", size = 1243258, upload-time = "2026-05-14T22:44:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/4810fbeb81c47b7e4e65db15ca65c71330efc59b460bd10c12338dc6012e/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91362c0a9fdf1c344b7f50a5b0508b11a0732102998fbd754a191f7187e77031", size = 1239226, upload-time = "2026-05-14T22:44:15.811Z" }, + { url = "https://files.pythonhosted.org/packages/28/38/13a88d90b664c009ed208346ec2ed248b0ab2cb0b582ae467acaa7f44fa4/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70d9c5d527bbfa69bd3c7d17dac11fb6781e36186a434a06d7d5892e0b2f88f9", size = 1448867, upload-time = "2026-05-14T22:44:17.99Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/a069dba1a634b703bf07fb49df8f7e3c04e9ba8ef3f0d9f4495f72630f92/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4738790cf54d8b416de992b87ee567056980bc82134d52458bd4985f389d1658", size = 1264135, upload-time = "2026-05-14T22:44:19.8Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/76ec4279fecd7e78b60c3c99321f944c43cd11e5ff09c952746f5f9c0f4c/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:faa008dccfcb793ae9101325e4d6d026caaa5d845c2182f03749c759834b0a3a", size = 1269060, upload-time = "2026-05-14T22:44:21.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/c5/9230ef7481e5cb63b93a1f7738e959586202b081caf32b8bc5d9f673ef56/ast_serialize-0.4.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c5245228e65d38cb48e1251f0ca71b0fa417e527141491e8c92f740e8e2d121", size = 1309654, upload-time = "2026-05-14T22:44:23.725Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/7d7397528d181ad68e476e0c81aa3ceff7d1f1b5c7fa958d6be28628ef16/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8f5153e9c44a02e61f4042c5f9249d2e8a759773d621a0b2f445a899e536e181", size = 1418855, upload-time = "2026-05-14T22:44:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8f/87d6428adaa0986b817404f09329b64f8d2614cfe061ebf4951b4a7e0d19/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1e1fb90def261f6a0db885876f7e1a49ad2dbac38ad9f2f62dba2f9543af16e7", size = 1516040, upload-time = "2026-05-14T22:44:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/b5/bb/5aaa41a21314c8b0d6dee54867b16535682c6660dd28cac64dba1380062d/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf2ff7b654c8e95143e20f5d75878cbb78b65b928b26c4d58ef71cdba9d6d981", size = 1511450, upload-time = "2026-05-14T22:44:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/87/16/cc729b5bb4b21da99db1379266cc367512e82ba10f9b3300a6f3e9941325/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90fc5c0d35a22f1a92dd33635508626d50f8fc64deb897c23e78e666a60804c9", size = 1463654, upload-time = "2026-05-14T22:44:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/43/97/7198321b0244d011093387b41affea934d58bda08d59a2adfde72976b6c4/ast_serialize-0.4.0-cp39-abi3-win32.whl", hash = "sha256:9ecd6a1fc1b86f1f4e8ae206759b6319c10019706b3496b01b54d02b9b2cd918", size = 1068636, upload-time = "2026-05-14T22:44:33.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/09/3b868f6d8df4bbe452903a5e0e039ebcec9ea0045f1a77951546205097e8/ast_serialize-0.4.0-cp39-abi3-win_amd64.whl", hash = "sha256:79c8d015c771c8bfdb1208003b227b27c40034790a2c29c09f2317a041825ce2", size = 1107137, upload-time = "2026-05-14T22:44:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/78/9387dffccdc55a12734f83aaccc4a987404a217a2a12a1920d8d4585950b/ast_serialize-0.4.0-cp39-abi3-win_arm64.whl", hash = "sha256:1026f565a7ab846337c630909089b3346a2fe417bf1552b1581ab01852137407", size = 1079199, upload-time = "2026-05-14T22:44:36.816Z" }, ] [[package]] @@ -155,7 +209,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146, upload-time = "2025-05-05T12:49:02.502Z" } wheels = [ @@ -186,17 +240,17 @@ version = "2.1.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h5py", version = "3.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.17.0", 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/cf/63/da18b1cdb9de07f8ad07d71535eac0460d065c71312ad400f55f6c2865ae/biom_format-2.1.17.tar.gz", hash = "sha256:8e3fa07a432b3f6d5c3cad491ef1f27b18d10fc151ca2d223761be4f0b050479", size = 11721758, upload-time = "2025-08-26T15:19:19.35Z" } wheels = [ @@ -232,7 +286,8 @@ name = "biopython" version = "1.85" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -274,12 +329,15 @@ wheels = [ [[package]] name = "biopython" -version = "1.86" +version = "1.87" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -290,56 +348,40 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/61/c59a849bd457c8a1b408ae828dbcc15e674962b5a29705e869e15b32bf25/biopython-1.86.tar.gz", hash = "sha256:93a50b586a4d2cec68ab2f99d03ef583c5761d8fba5535cb8e81da781d0d92ff", size = 19835323, upload-time = "2025-10-28T21:18:31.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/9f/5c95732ad98a6d40f4be58978be801cc87b50c71d79a7aee46c4a085114d/biopython-1.86-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02aef2e31cc92544f574ff837cabaaaf53733f3a6b5a433f781c59e5424a7576", size = 2691935, upload-time = "2025-10-28T21:27:12.558Z" }, - { url = "https://files.pythonhosted.org/packages/ca/15/9902cbc901073ba2de397f5c9c84e72147e02aaca1755fa650d26bb715a2/biopython-1.86-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e1b12819a78242b529f54e5d2d00ad90023710a5846ca0f2011ac989fd17d4b", size = 2669420, upload-time = "2025-10-28T21:26:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5d/9cb775106361f8ef7ab459b89ff6d725e81dae7abd382309d3bbf82ced6d/biopython-1.86-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e62504faac6e62fe26e40d6905a69519d8b7b5b0506a426d641b218fde788b5", size = 3183554, upload-time = "2025-10-29T00:35:24.074Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/87c8db55746099b38baf6c597688807bad2e59074cec37169168fe98e69e/biopython-1.86-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d4530060aadc6af060a9a049da91a582738837e187fcea80486c71eca74ae59", size = 3205260, upload-time = "2025-10-29T00:35:33.89Z" }, - { url = "https://files.pythonhosted.org/packages/88/6c/7822f6b7521073ccc80eb18736b664efe1d59edeb6a3f72c362f542ec352/biopython-1.86-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:186e2065c0d1a6c2afc85b9c21a2911a931949668bd73b4c03f429a31b3589f8", size = 3154783, upload-time = "2025-10-28T23:52:52.157Z" }, - { url = "https://files.pythonhosted.org/packages/98/7a/6759f99b481432969a4979f03b4c4966716d4cffd2154749ae5af0d8904a/biopython-1.86-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915a09859159598b9421e7240561692e7bb4084e5340c4dbb2435c5c38805a2", size = 3173920, upload-time = "2025-10-28T23:52:57.611Z" }, - { url = "https://files.pythonhosted.org/packages/17/c2/fe2a223b3fdd718099055679b68f9f15b2006d83a5a1c1593246d5e5fe81/biopython-1.86-cp310-cp310-win32.whl", hash = "sha256:be3d83152fe3232e2d197896a506902b84ad60d40b3f1d1fc934914d138c6dc1", size = 2697949, upload-time = "2025-10-28T21:32:07.305Z" }, - { url = "https://files.pythonhosted.org/packages/1f/11/44fb3975df1070966ffc213f52e9fff63fb48b43bc3d403584ca3f4f9a42/biopython-1.86-cp310-cp310-win_amd64.whl", hash = "sha256:6fbbfe19e12170754adb9632155b7e3be0d4c247f0a2e09d3917bec859282de1", size = 2734235, upload-time = "2025-10-28T21:32:03.014Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f5/37d6bb3a1245ec5f5f1c66d5cd790b06cdb54a75b36849893405c17f3612/biopython-1.86-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba88b0754ad53c93eba11d910364cfc773686933c89a886522309ba903151e50", size = 2691944, upload-time = "2025-10-28T21:27:24.053Z" }, - { url = "https://files.pythonhosted.org/packages/14/12/44d71f333b7302b30788df80705f2207c47b54c17d0935a378dfc709507d/biopython-1.86-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6cceb32b9036bbdc59962e31bd1605ece24edc226c0d50f99839948b5b5c9dda", size = 2669434, upload-time = "2025-10-28T21:26:49.145Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1b/731060090ed29b5ac2484865255f1f363a50afb7275717ceb2c6f20d3ea4/biopython-1.86-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0f040ff85bd7d0ee06574bc6d032bc666802f2fe781b0c316b936237eb3d17e", size = 3196718, upload-time = "2025-10-29T00:35:47.806Z" }, - { url = "https://files.pythonhosted.org/packages/1c/8d/8409535c341061b9c78faf151e73b484b456b3c3bdf59b27cf3984f16fbc/biopython-1.86-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ac858fd71f1093380d8b0a16acf060e7c228ad65f9ecacdb9f5760cfb9f59b1", size = 3218383, upload-time = "2025-10-29T00:35:53.523Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bc/5e93a11f70732122679747a728509d03a6a066b178cc1d7ca30ed2f1ebee/biopython-1.86-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4bcf5a48ee647624e2d0bedac7fb1c24ef0facd514519cca074593b8a6a40e", size = 3168368, upload-time = "2025-10-28T23:53:16.425Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/e187940571a3a24d20f407f1d7514ab1fe0dc9fa49e01790c4bd56ced0bc/biopython-1.86-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d4dd9090caaf364a08ab54cd561f37c5f4ea5bcc8f0189d332dcd36d6df5767", size = 3186451, upload-time = "2025-10-28T23:53:22.463Z" }, - { url = "https://files.pythonhosted.org/packages/2a/88/1e8ffb0db6a03888768613d682a79043e9975067b9095e644a6872905c88/biopython-1.86-cp311-cp311-win32.whl", hash = "sha256:90591f4554c09d311193e7774b5143442c67e178a5b7d929aaa2a054048b22a7", size = 2697756, upload-time = "2025-10-28T21:32:23.017Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b2/e34e45d6cb46c96486a2ed5f07874b6c9493dec68b9d6262ae05f4fe909b/biopython-1.86-cp311-cp311-win_amd64.whl", hash = "sha256:0a95321ca929c04c934e62252c9e2cc5c4fd13ce575798d98af2d79512334b9b", size = 2733781, upload-time = "2025-10-28T21:32:18.409Z" }, - { url = "https://files.pythonhosted.org/packages/98/e2/199b8ccbd4b9bf234157db0668177b5b7784d62f29d9096fd0d3a70e3b86/biopython-1.86-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f8d372aae21d79b11613751c6ae23c88db0e94d25b7567b1f67aa0304fb61667", size = 2693171, upload-time = "2025-10-29T00:26:59.028Z" }, - { url = "https://files.pythonhosted.org/packages/d8/2f/1a7da2a55212b3d0a03866d22213f91273fee3722b5364575419fbe574a5/biopython-1.86-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:baf19d9237aaaa387a68f8f055f978af5c80338d7e037ab028e8d768928f1250", size = 2692543, upload-time = "2025-10-28T21:27:31.855Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e9/4057d4c2aa22ca25c180ecbed2ce9e7d65bf787999778bc63b41df0d03b5/biopython-1.86-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f9abdf6cbf0087850de5f8148da0d420c4cb87905bf4de3145ad24a8d55dcd", size = 2669975, upload-time = "2025-10-28T21:26:54.181Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/3e6862720d7c51f0fbe7d6d25be72a95486779d9d98122283b4e8032fb40/biopython-1.86-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:187c3c24dd2255e7328f3e0523ab5d6350b73ff562517de0c1922385617101d2", size = 3209367, upload-time = "2025-10-29T00:36:06.522Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cb/61877367bf08670573d62513b239dc65cf2b7488dc74322cc6051da2e55e/biopython-1.86-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1859830b8262785c6b59dfe0c82cddb643974f63b9d2779bb9f3e2c47c0a95da", size = 3235466, upload-time = "2025-10-29T00:36:11.516Z" }, - { url = "https://files.pythonhosted.org/packages/84/1a/3182a77776b76f3f5c64825ee1acf9355f665bed72ee9e8ff49e48f25d98/biopython-1.86-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfd906c47b6fb38e3abb9f52e0c06822e6e82a043d38c2000773692c29db1ed8", size = 3178776, upload-time = "2025-10-28T23:53:41.487Z" }, - { url = "https://files.pythonhosted.org/packages/1a/22/828b08fac8dbc8c1dbc1ad03815137cebc9c78303ec7d21b568544028119/biopython-1.86-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6ab2c60742f1c8494cfbbe3b7a8b45f0400c8f2b36b686b895d5e4d625f04e", size = 3197586, upload-time = "2025-10-28T23:53:47.136Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/122aea7653fa93d7eb72978928e80759082efffa70afe0c25a17e18521da/biopython-1.86-cp312-cp312-win32.whl", hash = "sha256:192c61bc3d782c171b7d50bb7d8189d84790d6e3c4b24fd41d1d7ffc7d303efe", size = 2698043, upload-time = "2025-10-28T21:32:39.452Z" }, - { url = "https://files.pythonhosted.org/packages/a9/13/00db03b01e54070d5b0ec9c71eef86e61afa733d9af76e5b9b09f5dc9165/biopython-1.86-cp312-cp312-win_amd64.whl", hash = "sha256:35a6b9c5dcdfb5c2631a313a007f3f41a7d72573ba2b68c962e10ea92096ff3b", size = 2733610, upload-time = "2025-10-28T21:32:34.99Z" }, - { url = "https://files.pythonhosted.org/packages/fd/6e/84d6c66ab93095aa7adb998a8eef045328470eafd36b9237c4db213e587c/biopython-1.86-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb3a11a98e49428720dca227e2a5bdd57c973ee7c4df3cf6734c0aa13fd134c7", size = 2693185, upload-time = "2025-10-28T21:27:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/60386f2640f13765b1651f2f26d8b4f893c46ee663df3ca76eda966d4f6a/biopython-1.86-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e161f3d3b6e65fbfd1ce22a01c3e9fa9da789adde4972fd0cc2370795ea5357b", size = 2669980, upload-time = "2025-10-28T21:26:58.839Z" }, - { url = "https://files.pythonhosted.org/packages/dd/de/a39adb98a0552a257219503c236ef17f007598af55326c0d143db52e5a92/biopython-1.86-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aa8c9e92ee6fe59dfe0d2c2daf9a9eec6b812c78328caad038f79163c500218", size = 3209657, upload-time = "2025-10-29T00:36:28.842Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c7/b2e7aca3de8981f4ecb6ab1e0334c3c4a512e5e9898b57b3d8734b086da7/biopython-1.86-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593ec6a2a4fedec08ddcee1a8a0e0b0ed56835b2714904b352ec4a93d5b9d973", size = 3235774, upload-time = "2025-10-29T00:36:34.07Z" }, - { url = "https://files.pythonhosted.org/packages/52/ed/e6647b0b9cf2bb67347612e8e443b84378c44768a8d8439276e4ba881178/biopython-1.86-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2f9ebf9b14d67ca92f48779c4f0ba404c35dba3e8b9d6c34d1a3591c3b746d", size = 3178415, upload-time = "2025-10-28T23:54:05.475Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/f6a14b835842c66a52f212136a99416265f5ce76813d668ceac1cb306357/biopython-1.86-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:137fe9aafd93baa5127d17534b473f6646f92a883f52b34f7c306b800ac50038", size = 3197201, upload-time = "2025-10-28T23:54:10.462Z" }, - { url = "https://files.pythonhosted.org/packages/f2/73/0eac930016c509763c174a0e25e92e6d7a711f6f5de1f7001e54fd5c49f7/biopython-1.86-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e784dc8382430c9893aa084ca18fe8a8815b5811f1c324492ef3f4b54e664fff", size = 3145106, upload-time = "2025-10-28T23:54:15.235Z" }, - { url = "https://files.pythonhosted.org/packages/00/aa/26e836274d03402e8011b04a1714d4ac2f704add303a493e54d2d5646973/biopython-1.86-cp313-cp313-win32.whl", hash = "sha256:5329a777ba90ea624447173046e77c4df2862acc46eea4e94fe2211fe041750f", size = 2698051, upload-time = "2025-10-28T21:32:55.225Z" }, - { url = "https://files.pythonhosted.org/packages/ae/27/fa1f8fa57f2ac8fdc41d14ab36001b8ba0fce5eac01585227b99a4da0e9d/biopython-1.86-cp313-cp313-win_amd64.whl", hash = "sha256:f6f2f1dc75423b15d8a22b8eceae32785736612b6740688526401b8c2d821270", size = 2733649, upload-time = "2025-10-28T21:32:51.052Z" }, - { url = "https://files.pythonhosted.org/packages/a4/2d/5b87ab859d38f2c7d7d1f9df375b4734737c2ef62cf8506983e882419a30/biopython-1.86-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:236ca61aa996f12cbc65a8d6a15abfac70b9ee800656629b784c6a240e7d8dc0", size = 2694733, upload-time = "2025-10-29T00:27:49.142Z" }, - { url = "https://files.pythonhosted.org/packages/24/7e/a80fad6dbfa1335c506b1565d2b3fdd78cda705408a839c5583a9cfca8b6/biopython-1.86-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f96b7441f456c7eecad5c6e61e75b0db1435c489be7cc5e4f97dd4e60921747c", size = 2670131, upload-time = "2025-10-29T00:27:53.758Z" }, - { url = "https://files.pythonhosted.org/packages/2d/0a/6c12e9262b99f395bd66535c4a4203bd70833c11f47ac0730fca6ba2b5f8/biopython-1.86-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d53a78bf960397826219f08f87b061ad7f227527d19986e830eeab60d370b597", size = 3209810, upload-time = "2025-10-29T00:36:45.88Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f9/265211154d2bb4cffe78a57b8e57cfbb165cf41cf3d1b68e2a6b073b3b8a/biopython-1.86-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb86e4383c02fdb2571a38947153346e6f5cd38e22de1df40f54d2a3c51d02a8", size = 3235347, upload-time = "2025-10-29T00:36:51.164Z" }, - { url = "https://files.pythonhosted.org/packages/64/e5/58d8e48d3b4100a7fd8bae97f0dd7179c30f19861841d1a0bb7827e0033e/biopython-1.86-cp314-cp314-win32.whl", hash = "sha256:ffeba620c4786ea836efee235a9c6333b94e922b89de1449a4782dcc15246ff1", size = 2698198, upload-time = "2025-10-29T00:28:02.812Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ca/aa166eb588a2d4eea381c92e5a2a3d09b4b4887b0f0e8f3acf999fb88157/biopython-1.86-cp314-cp314-win_amd64.whl", hash = "sha256:efbb9bc4415a1e2c1c986ba261b02857bc0c9eed098b15493f1cc5c4a1e02409", size = 2734693, upload-time = "2025-10-29T00:27:58.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/da/8c227d701ec9c94d9870b1879982e3dd114da130b0816d3f9b937318d31a/biopython-1.86-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:caa70c1639b3306549605f9273753bdbf8cd6d6d352cecf23afbda3c911694f3", size = 2697389, upload-time = "2025-10-29T00:28:07.037Z" }, - { url = "https://files.pythonhosted.org/packages/8c/1e/66b0b5622ef6a3a14c449d1c8d69749480b37518e4c1e3a8a86fc668dad7/biopython-1.86-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d077f01d1f69f77a26cac46163d4ea45eb4e6509a68feb7f15e665b7e1de0a99", size = 2673857, upload-time = "2025-10-29T00:28:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/76/05/7c8f9800e6960da2007eb75128c8ec0b22e1a0064e8802e8acfad53cdca8/biopython-1.86-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4506ce7dbdf885cb24d1f5439362c3c07f1b6f90761a0d20fe16a2a9ea5702a5", size = 3253007, upload-time = "2025-10-29T00:36:56.066Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/a2177328d841fda0a12e67c65d06279691e25363a2805f561b3665cae114/biopython-1.86-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcd94717e83ba891ebd9acaecbf05ad38313095ca5706caf6c38fa3f2aa17528", size = 3272883, upload-time = "2025-10-29T00:37:01.189Z" }, - { url = "https://files.pythonhosted.org/packages/ce/04/1aa91f64db5e0728d596fcf7302e2ae2035800c0676e94ea09645a948b91/biopython-1.86-cp314-cp314t-win32.whl", hash = "sha256:2f6b205dcb4101cefa5c615114bd35a19f656abb9d340eb3cf190f829e43800a", size = 2701649, upload-time = "2025-10-29T00:28:20.527Z" }, - { url = "https://files.pythonhosted.org/packages/63/7c/4acaca39102d667175bb3d6502dea91c346f8674c06d5df0dbb678971596/biopython-1.86-cp314-cp314t-win_amd64.whl", hash = "sha256:efeee7c37f2331d2c55704df39e122189cc237ffd7511f34158418ad728131b8", size = 2741364, upload-time = "2025-10-29T00:28:15.752Z" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/3e/3c6aa8b2a7e6b791a34407736db32f59657001f0446ada31db73a3e0b7d5/biopython-1.87.tar.gz", hash = "sha256:8456c803459b679a9712422e5a7fd9809f2f089bf69bb085f3b077946ac9bdbf", size = 19855264, upload-time = "2026-03-30T11:28:29.823Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/d6/a01a222dcb2a624f186907f9d7ded1d263ca6164805b0986828f8be3832c/biopython-1.87-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:35f13796188412f135acbed196bd6cfedc1257199d50b4883e289bbd96320efc", size = 2680405, upload-time = "2026-03-30T11:37:21.908Z" }, + { url = "https://files.pythonhosted.org/packages/68/81/46d07aeac54d96e8beb20900fc8223bfae5ca5a9188d70e9f54cf704f36e/biopython-1.87-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63978e1b3ae040c52369bc1bd3f4c668171f5f1f0808798eccd74b575cce455d", size = 3194558, upload-time = "2026-03-30T11:37:27.573Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/7f5d251c133fd7f5f7dc57966b2116a15ffd1b8daf0a5144eda7419b2998/biopython-1.87-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e951f4862ffc1dccc28e1c25245059fa653d86028a88f5dfe1b7875875f3a4f", size = 3216226, upload-time = "2026-03-30T11:37:31.172Z" }, + { url = "https://files.pythonhosted.org/packages/23/5b/76a1a5bf4384201f2f146371f67b191b9a5000d7f3be1f962f7e829fecbd/biopython-1.87-cp310-cp310-win32.whl", hash = "sha256:86596b05f39afbc5984ee6239c93fd75f6a97fffb9a630d74b45652c24aab964", size = 2708959, upload-time = "2026-03-30T11:37:39.005Z" }, + { url = "https://files.pythonhosted.org/packages/b1/92/d09b783b7293567c9c662091c6dd69e9630657216deb441d1c4a9b20eeee/biopython-1.87-cp310-cp310-win_amd64.whl", hash = "sha256:efc16dd8a9312eb655fa590821495b0d8ca25d19f7b4ef4fa4da9e71d59d33e5", size = 2745184, upload-time = "2026-03-30T11:37:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/5cab8b1425d7361de75eb7395efb22f0e4a6eb899078fd008d2e748d4962/biopython-1.87-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58e36efa7eaa8813cffe440af4824afa20cc3c21b1b179a95cc0fb15c4b83c01", size = 2680402, upload-time = "2026-03-30T11:37:42.15Z" }, + { url = "https://files.pythonhosted.org/packages/58/09/717d55cc9d74b55fab79b283f1419b1c8785f19875e80f770737645838cc/biopython-1.87-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccf00d15e698656796ab14ff3eb175e282da7a08eedd36a29b6cacec5a33e97f", size = 3207745, upload-time = "2026-03-30T11:37:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c7/06ae2e0672ef5eae35b5858355118cc265553d0ba8a23eaa1c056359683d/biopython-1.87-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bab39ec4108fb2542a9f1012db8269426fd6e86ed84ebc1b0bd11134ef443dd3", size = 3229357, upload-time = "2026-03-30T11:37:48.472Z" }, + { url = "https://files.pythonhosted.org/packages/65/3b/77446506cec866f714e5a7c1f7613ced29762e5a6dfa6be99978dd68a273/biopython-1.87-cp311-cp311-win32.whl", hash = "sha256:126e18ad44e959a1984560562fa4f295c075ce2e610e8ddbc9ec6f52e09f70d8", size = 2708795, upload-time = "2026-03-30T11:37:53.923Z" }, + { url = "https://files.pythonhosted.org/packages/a5/76/12ac6fc6158d0e1ce92f96813dd00ae2770e2c47978c825c904c82058566/biopython-1.87-cp311-cp311-win_amd64.whl", hash = "sha256:d4ff5369ffb7a966bcf921cae6c8a6eab5070b5df1c9f2ae8c18ad40fdcb7ec5", size = 2744744, upload-time = "2026-03-30T11:37:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/18f024658c364196b7f9519674edd3233136fa19874b7ffd9e55ea0fd8e6/biopython-1.87-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:77ccc634621904d4a8fa0a43b5e0f093fa9df8c9577ed3858af648bb3528f51e", size = 2680980, upload-time = "2026-03-30T11:37:58.626Z" }, + { url = "https://files.pythonhosted.org/packages/c1/40/37c45bb4b5e345664bd970c3294349d1e35d4ad5794f808324bbef6ff9e7/biopython-1.87-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3428155c3e0abbed7aad5ff08e034d435b84dfe560c8ec58e7d43abda4b6a43", size = 3220352, upload-time = "2026-03-30T11:38:03.619Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/7898c2061966d6d62f0bb2e53cd5e1b3bb3053d2bc431f299802faca23cc/biopython-1.87-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:65ba69ef0273e983a9036c2a228142bc34266179a5f03660fc281d332d718630", size = 3246483, upload-time = "2026-03-30T11:38:07.185Z" }, + { url = "https://files.pythonhosted.org/packages/54/d3/a594c8443dc8979b3c051aa266aa4af2d39762c4bb2c37fe6892a19a7713/biopython-1.87-cp312-cp312-win32.whl", hash = "sha256:b077777fd2c555434bdcee58743f6f860aa80e1e005d9671913aa73823c6a773", size = 2709063, upload-time = "2026-03-30T11:38:13.53Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d5884c67b20d9ae2b8d93593c971363421fd04c3dc8f5c35530c18e1d6a7/biopython-1.87-cp312-cp312-win_amd64.whl", hash = "sha256:856e3d64f1f27db493474ff84916ed8572731a525e001c7d0d8f41a0fd187000", size = 2743967, upload-time = "2026-03-30T11:38:10.739Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/00b8005c24f7c36d8bdffae3354194a2221716004e39029528be923adeae/biopython-1.87-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e05ef5d632c319ab3ef77705c74061190d0792b07e1f2b9eee867401b2758e7e", size = 2680946, upload-time = "2026-03-30T11:38:16.872Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/59115001469e8b3decc8362e1e6e8201acd56cafab95f4f29f4d9994fb4c/biopython-1.87-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:772539297fa16a78f38651c793f53f8c11bd18317b111982e72cf30a6e57512a", size = 3220661, upload-time = "2026-03-30T11:38:20.091Z" }, + { url = "https://files.pythonhosted.org/packages/92/ea/dc2840df6f676d69e898792a0cd6f1217754333ec0003ad3ed5bc7c75da7/biopython-1.87-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d221b2e08e7e89713fdbfb15c8ea6744e908d59f672cd2b6fcf9ed47910d05e", size = 3246766, upload-time = "2026-03-30T11:38:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/6d/98/e7d0e9ed7509798286d6b043fb10a078616aff7c740ad0df506551992b09/biopython-1.87-cp313-cp313-win32.whl", hash = "sha256:fab1b12f6bc4646b7f56b4c390ecff685f02b5b29e3a0c10477195bb49fe62f8", size = 2709036, upload-time = "2026-03-30T11:38:28.822Z" }, + { url = "https://files.pythonhosted.org/packages/93/6e/50d9e4625d687696b3d44bba0d6ab41fe99eee74c97d5d6c5b00c20c03ad/biopython-1.87-cp313-cp313-win_amd64.whl", hash = "sha256:01ee30203bd4b2145cdfe2878499e549a7087f897a6f4d1ebd9de30790123140", size = 2743974, upload-time = "2026-03-30T11:38:26.38Z" }, + { url = "https://files.pythonhosted.org/packages/26/3f/1ed27b0991697993c8f20bcc6b9b55a720993213e4a1aa4803b21366e11b/biopython-1.87-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db73fe16aa2b20677ac86d1997612acb0aa1a3720bc899f65d2bce5583208e1b", size = 2681105, upload-time = "2026-03-30T11:38:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7f/4d405d34d9c6fe1852527eba5dc6c92d333ef739b297df71c771da821ecf/biopython-1.87-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ffe272517478691439a59cccd3cc2929fc8f6bfb8cbc8cc5acc103660395a7", size = 3220706, upload-time = "2026-03-30T11:38:34.572Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/9cade51cffba81b3c8dc314f146eb71c46c491d8092fef0f5bc4ccf3a66d/biopython-1.87-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff28a6f31630b3c9f52903478a2ed9dd894b07c1998e40eaeefbeefac20f2d0f", size = 3246379, upload-time = "2026-03-30T11:38:38.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/59/ad1adcd0b9a600ce9fa58ecf1587a75769f05d65320f5daf315ff490fcee/biopython-1.87-cp314-cp314-win32.whl", hash = "sha256:d740c75d4bc94f9dff51719a0deda37e5e885f06ee6dfbb5e9a21bbe9de35a9c", size = 2709283, upload-time = "2026-03-30T11:38:45.886Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e3/313d4002628ffec17745336ccfbfb59d746dec5a022ebd50a9b33c3113c6/biopython-1.87-cp314-cp314-win_amd64.whl", hash = "sha256:98e397096336a49804b6aaaeac8c47ad82e3e4430862f0cde37be73037f1017e", size = 2745825, upload-time = "2026-03-30T11:38:42.285Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/952fb76b38c314c5c7643d55dad10c71d45d5a6c78031795d594a4a1f46f/biopython-1.87-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e4878a9b56775480154c686f81e98f6d907b44d87605bdc2f53538ccdfde9624", size = 2684826, upload-time = "2026-03-30T11:38:48.947Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fb/93e4c0fc762a10cb0d41c9807a87ad6318c4d6d21b272aa160b6394f1ceb/biopython-1.87-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6fcec2e3602ed52ced701f8f7851952383f84dbc4caeb4d202d088170e86b6d", size = 3263913, upload-time = "2026-03-30T11:38:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/63/10/5a2794c558ef6058da8413ad0ffeee2c090d250b9e5d31009faadfe84fc5/biopython-1.87-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8da2d44a4b912c7550a051a5ff4bb72a61decc9c4b19ea92cba4c02fffb143c", size = 3283916, upload-time = "2026-03-30T11:38:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/cd/b18b40e9de9475cfd59f7c30dea6bab7f337758f617d3f5853b1ece8abee/biopython-1.87-cp314-cp314t-win32.whl", hash = "sha256:3670d76759c6cb53ba617f9823d3a438c1aa5415abef6addd29cb81d61d7b312", size = 2712763, upload-time = "2026-03-30T11:39:00.551Z" }, + { url = "https://files.pythonhosted.org/packages/8e/12/478812ef9c8484f96253ba8fb42b466db4f4594c3bb352a5f4318de01704/biopython-1.87-cp314-cp314t-win_amd64.whl", hash = "sha256:331c4151608a1d8406eff0d3c52a0ff1fa3e82604fc85f11c696c562919fb161", size = 2751774, upload-time = "2026-03-30T11:38:57.678Z" }, ] [[package]] @@ -347,7 +389,8 @@ name = "black" version = "25.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -390,12 +433,15 @@ wheels = [ [[package]] name = "black" -version = "26.1.0" +version = "26.3.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -405,43 +451,43 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, { name = "pathspec", marker = "python_full_version >= '3.10'" }, - { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytokens", marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, - { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, - { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, - { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, - { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, - { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, - { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -455,27 +501,62 @@ wheels = [ [[package]] name = "build" -version = "1.4.0" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.10' and os_name == 'nt'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pyproject-hooks", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/ec/bf5ae0a7e5ab57abe8aabdd0759c971883895d1a20c49ae99f8146840c3c/build-1.4.4.tar.gz", hash = "sha256:f832ae053061f3fb524af812dc94b8b84bac6880cd587630e3b5d91a6a9c1703", size = 89220, upload-time = "2026-04-22T20:53:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-py3-none-any.whl", hash = "sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d", size = 25921, upload-time = "2026-04-22T20:53:43.251Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and os_name == 'nt'" }, + { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.10.2'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pyproject-hooks", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, ] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -575,107 +656,123 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, - { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, - { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, - { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, - { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, - { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, - { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, - { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -683,7 +780,8 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, @@ -695,12 +793,15 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -712,9 +813,9 @@ resolution-markers = [ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -724,7 +825,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/ac/8c27720838e293898671f01b5c452236a0c74f4799a3f2d5fcccbbf50d71/cma-4.4.4.tar.gz", hash = "sha256:632bd654b5dce04c0eaa3166679d3e4773ce7a79eab7934e7f363c341b9a8170", size = 316645, upload-time = "2026-02-25T22:18:16Z" } wheels = [ @@ -757,7 +858,8 @@ name = "contourpy" version = "1.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -905,9 +1007,12 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -916,7 +1021,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", 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 = [ @@ -998,7 +1103,8 @@ name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ @@ -1114,12 +1220,15 @@ toml = [ [[package]] name = "coverage" -version = "7.13.3" +version = "7.14.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -1128,99 +1237,113 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, - { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, - { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, - { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, - { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, - { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, - { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, - { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, - { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, - { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, - { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, - { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, - { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, - { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, - { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, - { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, - { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, - { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, - { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, - { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, - { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, - { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, - { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, - { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, - { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, - { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, - { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, - { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, - { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, - { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, - { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, - { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, - { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, - { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, - { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, - { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, ] [package.optional-dependencies] @@ -1230,45 +1353,99 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.4" +version = "47.0.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version <= '3.9'", +] dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32') or (platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "cffi", marker = "python_full_version <= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version <= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, - { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, - { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, - { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, - { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, - { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, - { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, - { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, - { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, - { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, - { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, - { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, - { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, - { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, - { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, - { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, - { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", + "python_full_version > '3.9' and python_full_version < '3.10'", +] +dependencies = [ + { name = "cffi", marker = "(python_full_version > '3.9' and python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten') or (python_full_version > '3.9' and python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32') or (python_full_version > '3.9' and platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "python_full_version > '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, ] [[package]] @@ -1282,22 +1459,24 @@ wheels = [ [[package]] name = "dash" -version = "4.0.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, - { name = "importlib-metadata" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "nest-asyncio" }, { name = "plotly" }, - { name = "requests" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "retrying" }, { name = "setuptools" }, { name = "typing-extensions" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/dd/3aed9bfd81dfd8f44b3a5db0583080ac9470d5e92ee134982bd5c69e286e/dash-4.0.0.tar.gz", hash = "sha256:c5f2bca497af288f552aea3ae208f6a0cca472559003dac84ac21187a1c3a142", size = 6943263, upload-time = "2026-02-03T19:42:27.92Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/da/a13ae3a6528bd51a6901461dbff4549c6009de203d6249a89b9a09ac5cfb/dash-4.1.0.tar.gz", hash = "sha256:17a92a87b0c1eacc025079a705e44e72cd4c5794629c0a2909942b611faeb595", size = 6927689, upload-time = "2026-03-23T20:39:47.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8c/dd63d210b28a7589f4bc1e84880525368147425c717d12834ab562f52d14/dash-4.0.0-py3-none-any.whl", hash = "sha256:e36b4b4eae9e1fa4136bf4f1450ed14ef76063bc5da0b10f8ab07bd57a7cb1ab", size = 7247521, upload-time = "2026-02-03T19:42:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/2a/00/10b1f8b3885fc4add1853e9603af15c593fa0be20d37c158c4d811e868dc/dash-4.1.0-py3-none-any.whl", hash = "sha256:1af9f302bc14061061012cdb129b7e370d3604b12a7f730b252ad8e4966f01f7", size = 7232489, upload-time = "2026-03-23T20:39:40.658Z" }, ] [[package]] @@ -1336,7 +1515,8 @@ version = "0.21.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ @@ -1348,9 +1528,12 @@ name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -1400,21 +1583,21 @@ wheels = [ [[package]] name = "flask" -version = "3.1.2" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] @@ -1422,7 +1605,8 @@ name = "fonttools" version = "4.60.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } wheels = [ @@ -1487,12 +1671,15 @@ wheels = [ [[package]] name = "fonttools" -version = "4.61.1" +version = "4.63.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -1501,57 +1688,57 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, - { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, - { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, - { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, - { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, - { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, - { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, - { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] [[package]] @@ -1568,7 +1755,8 @@ name = "greenlet" version = "3.2.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/b0/f5/3e9eafb4030588337b2a2ae4df46212956854e9069c07b53aa3caabafd47/greenlet-3.2.5.tar.gz", hash = "sha256:c816554eb33e7ecf9ba4defcb1fd8c994e59be6b4110da15480b3e7447ea4286", size = 191501, upload-time = "2026-02-20T20:08:51.539Z" } wheels = [ @@ -1616,12 +1804,15 @@ wheels = [ [[package]] name = "greenlet" -version = "3.3.2" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -1630,54 +1821,54 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, - { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, - { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, - { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, - { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, - { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, + { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, ] [[package]] @@ -1685,7 +1876,8 @@ name = "h5py" version = "3.14.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -1721,12 +1913,15 @@ wheels = [ [[package]] name = "h5py" -version = "3.15.1" +version = "3.16.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -1737,49 +1932,57 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/30/8fa61698b438dd751fa46a359792e801191dadab560d0a5f1c709443ef8e/h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b", size = 3414477, upload-time = "2025-10-16T10:33:24.201Z" }, - { url = "https://files.pythonhosted.org/packages/16/16/db2f63302937337c4e9e51d97a5984b769bdb7488e3d37632a6ac297f8ef/h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422", size = 2850298, upload-time = "2025-10-16T10:33:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2e/f1bb7de9b05112bfd14d5206090f0f92f1e75bbb412fbec5d4653c3d44dd/h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5", size = 4523605, upload-time = "2025-10-16T10:33:31.168Z" }, - { url = "https://files.pythonhosted.org/packages/05/8a/63f4b08f3628171ce8da1a04681a65ee7ac338fde3cb3e9e3c9f7818e4da/h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9", size = 4735346, upload-time = "2025-10-16T10:33:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/74/48/f16d12d9de22277605bcc11c0dcab5e35f06a54be4798faa2636b5d44b3c/h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883", size = 4175305, upload-time = "2025-10-16T10:33:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/47cdbff65b2ce53c27458c6df63a232d7bb1644b97df37b2342442342c84/h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe", size = 4653602, upload-time = "2025-10-16T10:33:42.188Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/dc08de359c2f43a67baa529cb70d7f9599848750031975eed92d6ae78e1d/h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7", size = 2873601, upload-time = "2025-10-16T10:33:45.323Z" }, - { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, - { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, - { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, - { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, - { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, - { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, - { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, - { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, - { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, - { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, - { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, - { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, - { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, - { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, - { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, - { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, + { name = "numpy", version = "2.4.5", 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" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, ] [[package]] @@ -1787,7 +1990,8 @@ name = "id" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "urllib3" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } wheels = [ @@ -1796,34 +2000,93 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] name = "imagesize" -version = "1.4.1" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/59/4b0dd64676aa6fb4986a755790cb6fc558559cf0084effad516820208ec3/imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f", size = 1281127, upload-time = "2026-03-03T01:59:54.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b1/a0662b03103c66cf77101a187f396ea91167cd9b7d5d3a2e465ad2c7ee9b/imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899", size = 5763, upload-time = "2026-03-03T01:59:52.343Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "zipp", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + [[package]] name = "importlib-resources" version = "6.5.2" @@ -1841,7 +2104,8 @@ name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ @@ -1853,9 +2117,12 @@ name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -1883,7 +2150,8 @@ name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "more-itertools" }, + { name = "more-itertools", version = "10.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "more-itertools", version = "11.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ @@ -1892,28 +2160,90 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.1.0" +version = "6.1.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] dependencies = [ - { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, + { name = "backports-tarfile", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, + { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, ] [[package]] name = "jaraco-functools" version = "4.4.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] dependencies = [ - { name = "more-itertools" }, + { name = "more-itertools", version = "10.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] +[[package]] +name = "jaraco-functools" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "more-itertools", version = "11.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, +] + [[package]] name = "jeepney" version = "0.9.0" @@ -1942,26 +2272,26 @@ source = { editable = "." } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.optional-dependencies] all = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "biopython", version = "1.87", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scikit-bio", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-bio", version = "0.7.1.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "scikit-bio", version = "0.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "seaborn" }, ] analysis = [ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "seaborn" }, ] benchmark = [ @@ -1969,7 +2299,7 @@ benchmark = [ { name = "kneed" }, { name = "optuna" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "plotly" }, { name = "pymoo", version = "0.6.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pymoo", version = "0.6.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -1978,30 +2308,32 @@ benchmark = [ ] biopython = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "biopython", version = "1.87", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] dev = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "biopython", version = "1.87", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "black", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "build" }, + { name = "black", version = "26.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "build", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "build", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "flake8" }, - { name = "mypy" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mypy", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "rich" }, { name = "scikit-bio", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-bio", version = "0.7.1.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "scikit-bio", version = "0.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "twine" }, ] docs = [ { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, @@ -2010,15 +2342,15 @@ docs = [ ] io = [ { name = "biopython", version = "1.85", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "biopython", version = "1.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "biopython", version = "1.87", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] skbio = [ { name = "scikit-bio", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-bio", version = "0.7.1.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "scikit-bio", version = "0.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] test = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, @@ -2027,9 +2359,10 @@ test = [ [package.dev-dependencies] dev = [ - { name = "build" }, + { name = "build", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "build", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, ] @@ -2090,10 +2423,13 @@ name = "keyring" version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, + { name = "jaraco-context", version = "6.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jaraco-context", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jaraco-functools", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jaraco-functools", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" }, @@ -2109,7 +2445,8 @@ name = "kiwisolver" version = "1.4.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } wheels = [ @@ -2209,12 +2546,15 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.9" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -2223,220 +2563,250 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, - { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, - { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, - { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, - { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, - { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, - { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, - { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, - { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, - { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, - { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, - { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, - { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, - { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, - { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, - { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, - { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, - { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, - { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, - { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, - { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, - { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, - { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, - { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, - { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, - { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +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/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { 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 = "kneed" -version = "0.8.5" +version = "0.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.17.0", 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/ba/0f/958e27a378042e0366dfea8baab4a53121cb37c114117666051390cd7bb8/kneed-0.8.5.tar.gz", hash = "sha256:a4847ac4f1d04852fea278d5de7aa8bfdc3beb7fbca4a182fec0f0efee43f4b1", size = 12783, upload-time = "2023-07-09T01:51:08.93Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/64/4bb8f8a7a4627b585a66d5bec0c9b30ae5b39a4caea1775c8bfb3fb3f4cf/kneed-0.8.6.tar.gz", hash = "sha256:65b22727c623661701f15edf057f2e6c73e2b1ad4e68cd9ca4291675c318b5ef", size = 13161, upload-time = "2026-03-20T21:01:51.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/1b/7e726d8616e813007874468c61790099ba21493e0ea07561b7d9fc53151c/kneed-0.8.5-py3-none-any.whl", hash = "sha256:2f3fbd4e9bd808e65052841448702c41ea64d5fc78735cbfc97ab25f08bd9815", size = 10290, upload-time = "2023-07-09T01:51:07.548Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cd/23c89d53c36028bccb39f55aa5dd24c4bdaab76c4d556ad43dc8cf026918/kneed-0.8.6-py3-none-any.whl", hash = "sha256:3412e7b70bce07717386d24fab37f0f985968d1b85ea0c749a6b98caccaf65ec", size = 10797, upload-time = "2026-03-20T21:01:50.87Z" }, ] [[package]] name = "librt" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, - { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, - { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, - { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, - { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, - { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, - { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, - { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, - { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, - { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, - { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, - { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, - { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, - { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, - { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, - { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, - { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, - { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, - { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, - { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, - { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, - { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, - { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, - { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, - { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, - { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, - { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, - { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, - { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, - { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, + { url = "https://files.pythonhosted.org/packages/66/54/5d5f27cc840d2d8a64d60e0650dba14044a95d85a875e42af2eb104ac8b9/librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f", size = 142475, upload-time = "2026-05-10T18:17:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/f9/72/535efe79cf47f70975e0b14ceb3b7984bb7e8b97fb2867d3979771be0b6a/librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677", size = 143365, upload-time = "2026-05-10T18:17:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/83/cc/4130d462aeaf190357517d2a48a0a25030fbfd604230f6c45908452fff9c/librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab", size = 475743, upload-time = "2026-05-10T18:17:10.822Z" }, + { url = "https://files.pythonhosted.org/packages/62/e8/3c8000edefeb443fd2139692fb966f6c5556cb1032c44f734550896df3b9/librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0", size = 467088, upload-time = "2026-05-10T18:17:12.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a1/6de754256493924874e5fa6c0f4f990d8b101c38d974589020d9dc3d02af/librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1", size = 496277, upload-time = "2026-05-10T18:17:13.662Z" }, + { url = "https://files.pythonhosted.org/packages/92/fb/c34cb5358d6f993f85014045decd6dccd089a6f11d188660e062ee6262ff/librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e", size = 489320, upload-time = "2026-05-10T18:17:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/7761d70841bac875be9627496546b2eccbdeb07da3e42431bc4a40cf0819/librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa", size = 510221, upload-time = "2026-05-10T18:17:16.595Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/af9d4ac1057cd4e472b89553924b528b3d34afa6b7167645b7e6db39596b/librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1", size = 516650, upload-time = "2026-05-10T18:17:18.245Z" }, + { url = "https://files.pythonhosted.org/packages/86/f4/08faaf48ce0833d3717ebe0a0054c09a05df1bc83ee2715113c9901cc147/librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5", size = 496622, upload-time = "2026-05-10T18:17:19.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/11/ec3e390627f70477093909875a38843c826ee2ff554d1649645c7cc59248/librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192", size = 538049, upload-time = "2026-05-10T18:17:21.221Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a7/649401dae7ea8645dd218aa2d9c351afa7b9e0645f07dc8776a1972c0cad/librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f", size = 100360, upload-time = "2026-05-10T18:17:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7e/6a9711d78f338445e36992a90071962294f5bab388b554ef8a313e6412dd/librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3", size = 118407, upload-time = "2026-05-10T18:17:24.01Z" }, ] [[package]] name = "mako" -version = "1.3.10" +version = "1.3.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] [[package]] @@ -2445,7 +2815,8 @@ version = "3.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "mdurl", marker = "python_full_version < '3.11'" }, @@ -2457,12 +2828,15 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -2473,9 +2847,9 @@ resolution-markers = [ dependencies = [ { name = "mdurl", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -2579,7 +2953,8 @@ name = "matplotlib" version = "3.9.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -2639,12 +3014,15 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.8" +version = "3.10.9" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -2657,71 +3035,71 @@ dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", version = "4.61.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fonttools", version = "4.63.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "kiwisolver", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyparsing", marker = "python_full_version >= '3.10'" }, { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, ] -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/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, - { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, - { 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, ] [[package]] @@ -2738,7 +3116,8 @@ name = "mdit-py-plugins" version = "0.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -2750,12 +3129,15 @@ wheels = [ [[package]] name = "mdit-py-plugins" -version = "0.5.0" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -2766,11 +3148,11 @@ resolution-markers = [ ] dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] @@ -2784,44 +3166,76 @@ wheels = [ [[package]] name = "moocore" -version = "0.2.0" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/34/19341fe4ee06a82bf364fa7ac0998ec0cc67750133b55de3564312971116/moocore-0.2.0.tar.gz", hash = "sha256:3dc601f85f9a4743ed50ddd027dca30e3bb55c899916a092c2ece495b1b2de08", size = 404160, upload-time = "2026-01-11T11:43:18.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/95/0e36dbb8f44690a3540ca3c8c08f9000799d95a5eeec2a49abc7c76a8a0f/moocore-0.3.1.tar.gz", hash = "sha256:a8f83cfbc0aa81c1c9dd33e473adbc9b638dc3b1a6943753f8146770bb76bae4", size = 424672, upload-time = "2026-05-04T21:39:12.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/aa/25a060b31e3b2f54d63f78331a324875c08226e1841da43ec3f371cf8e17/moocore-0.2.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:653449231f328d3c9e69693ec3d44e8c77f38ab7e9ef0c69dd9ded40449e980d", size = 565572, upload-time = "2026-01-11T11:43:06.08Z" }, - { url = "https://files.pythonhosted.org/packages/ef/0d/a37abf346507a81554e43c8cfaedceac4776d8d29803a86e32d9eecfaafb/moocore-0.2.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8f091a7304532ed605acd82acd051e89af22ece8e2a27a3cee0faf9f2ea185", size = 743600, upload-time = "2026-01-11T11:43:08.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/aa/30e081884a653ab7f4a6bd9da99824c5d641c369928cee3cac4a7d801f36/moocore-0.2.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e93c07062adefd0fcba73a521f325f7fb874f2af92aaeec203cf9db31a41894b", size = 731717, upload-time = "2026-01-11T11:43:11.485Z" }, - { url = "https://files.pythonhosted.org/packages/52/9f/31d86de8a3bc21100b8a8b4c56d7d5a93f78e3c32c3c4a3395d985eb6baf/moocore-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90c7bde2164f9b95c6b2e870f0ca6ccc5dabff2bf8086162d7318c770e5868f", size = 737006, upload-time = "2026-01-11T11:43:13.024Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/79f0a968595c1d8a804fa9c66aa21cf676ffa9aacedf2ea50bd35d7f83d6/moocore-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d0699b770b5eebdeac477a356d539efa8807c6cf067a453a0682e0df2299a512", size = 724847, upload-time = "2026-01-11T11:43:14.275Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/535c1d448dbfa8ee79a83c36a6ded5a54ea35d02fac1fe2907ae540e369b/moocore-0.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:ea057409731e73dbc4ba4214cbf7747309695b01314f8786678b758cc9c561c4", size = 485644, upload-time = "2026-01-11T11:43:15.598Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ca/29bdef14758bfe869b74a0eae94d1a1ce220f75ce26ed6e7f4965ca70b49/moocore-0.2.0-cp310-abi3-win_arm64.whl", hash = "sha256:a7683feddfd2a47b4a0f89ee8d370cae72331792f68e67f083ccb37bb2f1c8cf", size = 476178, upload-time = "2026-01-11T11:43:16.919Z" }, + { url = "https://files.pythonhosted.org/packages/76/fd/da7e96fca6a05f5a80b682aa75f301e2ce870836fc444973a2c717beb945/moocore-0.3.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:cf22852e951e66cb1145858ff1e4fabcb2810dfc2c19d8df99bdc1a52b28c591", size = 631735, upload-time = "2026-05-04T21:39:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/40/df/c4715242db5a0d0e17578c5e643334c17d7760cd9f66c17b76a3b8267fb5/moocore-0.3.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14af334b83ddf6fbdec8e5ab6e3246e85ad6b34b0f1b2c44a52e3c5561ff5530", size = 882475, upload-time = "2026-05-04T21:39:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/34/74/6230c8c2865586ce4c4b0eea994bccadd9e3e28f6acd9cade123ba8516fa/moocore-0.3.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd0a65e501e704e5270d564c3b32438ddac34d22d3c5c54b01b8c897f4d949aa", size = 866948, upload-time = "2026-05-04T21:39:05.2Z" }, + { url = "https://files.pythonhosted.org/packages/23/10/6cbbc1039ce33a862d6fa7e24d381635ffd183a3e5a2036bad8477cb9c62/moocore-0.3.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3fd980c39e8ad4ba12147ac01f700ec402fb25d452beeccc1a7dc2e0b61e95ca", size = 875147, upload-time = "2026-05-04T21:39:06.846Z" }, + { url = "https://files.pythonhosted.org/packages/14/17/0ead0e1ea08fdc9a980c2525c61e034d5161675cbb320eae61028e93868d/moocore-0.3.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b9ddc86e6949d5b5466e805d474b5ae2dbb866ae218dfe7b53cee9eed0151ff", size = 862014, upload-time = "2026-05-04T21:39:08.232Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/a21b67ddaddb89843869b78e7691d20eab5c5b869a2908cda86fd0d8f663/moocore-0.3.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d5b666bf80bc972816ba8586de77ac6eeaf2b55ab493310487c14e90d491650", size = 517463, upload-time = "2026-05-04T21:39:09.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/dc/cd6f4a8977011db9a62c7981462e1820593ee44720a1f3c9a7abe8a94421/moocore-0.3.1-cp310-abi3-win_arm64.whl", hash = "sha256:52178eaaa12babe9532c5494d619cb44c791660adc7ae70dbd2daccf37b7990f", size = 505777, upload-time = "2026-05-04T21:39:11.206Z" }, ] [[package]] name = "more-itertools" version = "10.8.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] +[[package]] +name = "more-itertools" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, +] + [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] dependencies = [ - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, + { name = "librt", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.10'" }, + { name = "pathspec", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ @@ -2864,6 +3278,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "ast-serialize", marker = "python_full_version >= '3.10'" }, + { name = "librt", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, + { name = "pathspec", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2878,7 +3366,8 @@ name = "myst-parser" version = "3.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -2904,7 +3393,7 @@ dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "mdit-py-plugins", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "pyyaml", marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, ] @@ -2915,12 +3404,15 @@ wheels = [ [[package]] name = "myst-parser" -version = "5.0.0" +version = "5.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -2931,24 +3423,24 @@ resolution-markers = [ dependencies = [ { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mdit-py-plugins", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml", marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, ] [[package]] name = "narwhals" -version = "2.16.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/0e/3ad61eb87088cc4932e0d851531fa82f845a6230b68b091a0e298cc7e537/narwhals-2.21.0.tar.gz", hash = "sha256:7c6e7f50528e62b7a967dd864d7e117d2955d38d4f730653ce46a9861358e2dc", size = 633083, upload-time = "2026-05-08T12:29:02.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl", hash = "sha256:1e6617d0fca68ae1fda29e5397c4eaacd3ffc9fffe6bcd6ded0c690475e853be", size = 451943, upload-time = "2026-05-08T12:29:01.058Z" }, ] [[package]] @@ -2971,35 +3463,36 @@ wheels = [ [[package]] name = "nh3" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, - { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, - { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, - { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, - { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, - { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, - { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, - { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, - { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, - { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, - { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, - { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, - { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/5f/1d19bdc7d27238e37f3672cdc02cb77c56a4a86d140cd4f4f23c90df6e16/nh3-0.3.5.tar.gz", hash = "sha256:45855e14ff056064fec77133bfcf7cd691838168e5e17bbef075394954dc9dc8", size = 20743, upload-time = "2026-04-25T10:44:16.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b0/8587ac42a9627ab88e7e221601f1dfccbf4db80b2a29222ea63266dc9abc/nh3-0.3.5-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:23a312224875f72cd16bde417f49071451877e29ef646a60e50fcb69407cc18a", size = 1420126, upload-time = "2026-04-25T10:43:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/1dbc4d0c43f12e8c1784ede17eaee6f061d4fbe5505757c65c49b2ceab95/nh3-0.3.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387abd011e81959d5a35151a11350a0795c6edeb53ebfa02d2e882dc01299263", size = 793943, upload-time = "2026-04-25T10:43:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/47/9f/d6758d7a14ee964bf439cc35ae4fa24a763a93399c8ef6f22bd11d532d29/nh3-0.3.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48f45e3e914be93a596431aa143dedf1582557bf41a58153c296048d6e3798c9", size = 841150, upload-time = "2026-04-25T10:43:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/d5d1ae8374612c98f390e1ea7c610fa6c9716259a03bbf4d15b269f40073/nh3-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0a09f51806fd51b4fedbf9ea2b61fef388f19aef0d62fe51199d41648be14588", size = 1008415, upload-time = "2026-04-25T10:43:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/d13a9c3fd2d9c131a2a281737380e9379eb0f8c33fea24c2b923aaafbb15/nh3-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c357f1d042c67f135a5e6babb2b0e3b9d9224ff4a3543240f597767b01384ffd", size = 1092706, upload-time = "2026-04-25T10:43:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/2f3add7f8680fcc896afa6a675cb2bab09982853ee8af40bad621f6b61c4/nh3-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:38748140bf76383ab7ce2dce0ad4cb663855d8fbc9098f7f3483673d09616a17", size = 1048346, upload-time = "2026-04-25T10:43:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c3/2f9e4ffa82863074d1361bfe949bc46393d91b3411579dfbbd090b24cac5/nh3-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:84bdeb082544fbcb77a12c034dd77d7da0556fdc0727b787eb6214b958c15e29", size = 1029038, upload-time = "2026-04-25T10:43:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/e8/10/2804deb3f3315184c9cae41702e293c87524b5a21f766b07d7fe3ffbcfbb/nh3-0.3.5-cp314-cp314t-win32.whl", hash = "sha256:c3aae321f67ae66cff2a627115f106a377d4475d10b0e13d97959a13486b9a88", size = 603263, upload-time = "2026-04-25T10:43:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/f6685248b49f7548fc9a8c335ab3a52f68610b72e8a61576447151e4e2e6/nh3-0.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c88605d8d468f7fc1b31e06129bc91d6c96f6c621776c9b504a0da9beac9df5f", size = 616866, upload-time = "2026-04-25T10:43:51.005Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/d8c9018635d4acfefde6b68470daa510eed715a350cbaa2f928ba0609f81/nh3-0.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:72c5bdedec27fa33de6a5326346ea8aa3fe54f6ac294d54c4b204fb66a9f1e79", size = 602566, upload-time = "2026-04-25T10:43:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/d162e99746a2fb1d98bb0ef23af3e201b156cf09f7de867c7390c8fe1c06/nh3-0.3.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3bb854485c9b33e5bb143ff3e49e577073bc6bc320f0ff8fc316dd89c0d3c101", size = 1442393, upload-time = "2026-04-25T10:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/25/8c/072120d506978ab053e1732d0efa7c86cb478fee0ee098fda0ac0d31cb34/nh3-0.3.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d401ab2d8e86d59e2126e3ab2a2f45840c405842b626d9a51624b3a33b6878", size = 837722, upload-time = "2026-04-25T10:43:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/d4e06e28c5ad1c4b065f89737d02631bd49f1660b6ebcf17a87ffcd201da/nh3-0.3.5-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acfd354e61accbe4c74f8017c6e397a776916dfe47c48643cf7fd84ade826f93", size = 822872, upload-time = "2026-04-25T10:43:56.581Z" }, + { url = "https://files.pythonhosted.org/packages/0a/62/50659255213f241ec5797ae7427464c969397373e83b3659372b341ae869/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52d877980d7ca01dc3baf3936bf844828bc6f332962227a684ed79c18cce14c3", size = 1100031, upload-time = "2026-04-25T10:43:58.098Z" }, + { url = "https://files.pythonhosted.org/packages/00/7a/a12ae77593b2fcf3be25df7bc1c01967d0de448bdb4b6c7ec80fe4f5a74f/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:207c01801d3e9bb8ec08f08689346bdd30ce15b8bf60013a925d08b5388962a4", size = 1057669, upload-time = "2026-04-25T10:43:59.328Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/5647dc04c0233192a3956fc91708822b21403a06508cacf78083c68e7bf0/nh3-0.3.5-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea232933394d1d58bf7c4bb348dc4660eae6604e1ae81cd2ba6d9ed80d390f3b", size = 914795, upload-time = "2026-04-25T10:44:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0e/bf298920729f216adcb002acf7ea01b90842603d2e4e2ce9b900d9ee8fab/nh3-0.3.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3a787dc76b50de6bee54ef242f26c41dfe47654428e3e94f0fae5bb6dd2cc1", size = 806976, upload-time = "2026-04-25T10:44:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/85/01/26761e1dc2b848e65a62c19e5d39ad446283287cd4afddc89f364ab86bc9/nh3-0.3.5-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:488928988caad25ba14b1eb5bc74e25e21f3b5e40341d956f3ce4a8bc19460dc", size = 834904, upload-time = "2026-04-25T10:44:03.454Z" }, + { url = "https://files.pythonhosted.org/packages/33/53/0766113e679540ac1edc1b82b1295aecd321eeb75d6fead70109a838b6ee/nh3-0.3.5-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c069570b06aa848457713ad7af4a9905691291548c4466a9ad78ee95808382b", size = 857159, upload-time = "2026-04-25T10:44:05.003Z" }, + { url = "https://files.pythonhosted.org/packages/58/36/734d353dfaf292fed574b8b3092f0ef79dc6404f3879f7faaa61a4701fad/nh3-0.3.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eeedc90ed8c42c327e8e10e621ccfa314fc6cce35d5929f4297ff1cdb89667c4", size = 1018600, upload-time = "2026-04-25T10:44:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/d9c59c1b49669fcb7bababa55df82385f029ad5c2651f583c3a1141cfdd1/nh3-0.3.5-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:de8e8621853b6470fe928c684ee0d3f39ea8086cebafe4c416486488dea7b68d", size = 1103530, upload-time = "2026-04-25T10:44:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/cdd210bfb8d9d43fb02fc3c868336b9955934d8e15e66eb1d15a147b8af0/nh3-0.3.5-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:6ea58cc44d274c643b83547ca9654a0b1a817609b160601356f76a2b744c49ad", size = 1061754, upload-time = "2026-04-25T10:44:09.362Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/7a39e72e668c8445bdd95e494b3e21cfdddc68329be8ea3522c8befb46c4/nh3-0.3.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e49c9b564e6bcb03ecd2f057213df9a0de15a95812ac9db9600b590db23d3ae9", size = 1040938, upload-time = "2026-04-25T10:44:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/4c/fc2f9ed208a3801a319f59b5fea03cdc20cf3bd8af14be930d3a8de01224/nh3-0.3.5-cp38-abi3-win32.whl", hash = "sha256:559e4c73b689e9a7aa97ac9760b1bc488038d7c1a575aa4ab5a0e19ee9630c0f", size = 611445, upload-time = "2026-04-25T10:44:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/db/1a/e4c9b5e2ae13e6092c9ec16d8ca30646cb01fcdea245f36c5b08fd21fbd5/nh3-0.3.5-cp38-abi3-win_amd64.whl", hash = "sha256:45e6a65dc88a300a2e3502cb9c8e6d1d6b831d6fba7470643333609c6aab1f30", size = 626502, upload-time = "2026-04-25T10:44:13.682Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/19cd0671d1ba2762fb388fc149697d20d0568ccfeef833b11280a619e526/nh3-0.3.5-cp38-abi3-win_arm64.whl", hash = "sha256:8f85285700a18e9f3fc5bff41fe573fa84f81542ef13b48a89f9fecca0474d3b", size = 611069, upload-time = "2026-04-25T10:44:14.934Z" }, ] [[package]] @@ -3007,7 +3500,8 @@ name = "numpy" version = "2.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } wheels = [ @@ -3124,12 +3618,15 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.2" +version = "2.4.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3137,79 +3634,78 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/44/1383ee4d1e916a9e610e46c876b5c83ea023526117d23cd911983929ec34/numpy-2.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3176dc8ff71dbb593606f91a69ad0c3cd3303c7eb546af477370ab9edf760288", size = 16969261, upload-time = "2026-05-15T20:22:23.036Z" }, + { url = "https://files.pythonhosted.org/packages/3d/61/54bacfbec7550bc398e6b6d9a861db35d64f75844e1d7920f5722c3cd5e7/numpy-2.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1811150e5148f5a01a7cc282cb2f489b4a3050a773e173adb480e507bad3a3d7", size = 14964009, upload-time = "2026-05-15T20:22:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/7a/55/fe86c64561761f185339c26001164a2687bd4787af681e961431abd2d534/numpy-2.4.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0d63a780070871210853ba01e90b88f9b85cf2abf63a7f143d5127189265ddf6", size = 5469106, upload-time = "2026-05-15T20:22:28.13Z" }, + { url = "https://files.pythonhosted.org/packages/2f/74/cf29b8317627f0e3aa2c9fb332d386bd734308cecd9e07da9f407d9ce0c3/numpy-2.4.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:0c6919cefafb3b76cd46a89dbb203bf1dd95529d2a6d09fef2d325d95d6a79d8", size = 6798945, upload-time = "2026-05-15T20:22:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b61730a17fa87d5abb13ce560a1b4ce3485d37a13e03eb7b414e598e72f8/numpy-2.4.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d51efede1e58e8b11877536a5518f60e318d8ff69b89ad7b38ee5e431b24d772", size = 15967025, upload-time = "2026-05-15T20:22:32.328Z" }, + { url = "https://files.pythonhosted.org/packages/03/39/70bcd187eb4d223c21fde02c2bdfbffbffef3288cbb3947c04c74ae39a08/numpy-2.4.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07ce7e74da92d7c71b5df157b9758bcdd53d7fea10602154de3afd2b3ddc34dd", size = 16918685, upload-time = "2026-05-15T20:22:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/400fd1315bbe228af3937cf8a74e32023df6217af36077919d00adc382e4/numpy-2.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d7828234a13185effb34979e146f9921f2a65dfbbe215e6dbb57d6478fc8e059", size = 17322963, upload-time = "2026-05-15T20:22:37.557Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/bbbafb657e6f6ee826b4ecdb8722a2e0aae4a981888eaf59eae6a535cc13/numpy-2.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f96083adc3dfc1bbf778f2c79654d88115fa07074c97cb724fe9508f12d91c55", size = 18651594, upload-time = "2026-05-15T20:22:40.449Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/857a515154a2a18b0dfae04089600d166d352d473ec17a0680d879582d06/numpy-2.4.5-cp311-cp311-win32.whl", hash = "sha256:4ed78c904a638b6e5d7cd4db90c06fca5fc6ec2f28d258305368f454a50e79cf", size = 6233849, upload-time = "2026-05-15T20:22:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/d215f3fb93541617adb5d58b3b9508e8a6413e499711e0adc0b80bcb445d/numpy-2.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:079b0fad6f2899b23c5da89792b5409d2d83fc83e8bd5c2299cc9c397a264864", size = 12608238, upload-time = "2026-05-15T20:22:45.229Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c4/611d66d3fcfa931954d37a19ce5575f3283d023e89ff0df6ad43b334ae9c/numpy-2.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:d6c78e260b53affe9b395a9d54fc61f101f9521c4d9452c7e9e3718b19e2215b", size = 10479452, upload-time = "2026-05-15T20:22:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/3275231e98620002681c922e792db04d72c356e9d8073c387344fc0e4ff1/numpy-2.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:654fb8674b61b1c4bd568f944d13a908566fdcb0d797303521d4149d16da05ef", size = 16689166, upload-time = "2026-05-15T20:22:50.761Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/000aab6a16bdec53307f0f72546b57a3ac9266a62d8c257bee97d85fd078/numpy-2.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4cd9f6fa7ce10dc4627f2bb81dd9075dab67e94632e04c2b638e12575ddaa862", size = 14699514, upload-time = "2026-05-15T20:22:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/ddaf3af9c46966fef5be879256f213d85a0c56c75d07a3b7defec7cf6b4c/numpy-2.4.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4f5bc96d35d94e4ceab8b38a92241b4611e95dc44e63b9f1fa2a331858ee3507", size = 5204601, upload-time = "2026-05-15T20:22:56.257Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/627fadd11959b3c7759008f34c92a35af8ff942dd8284a66ced648bbe516/numpy-2.4.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4bb33e900ee81730ad77a258965134aa8ceac805124f7e5229347beda4b8d0aa", size = 6551360, upload-time = "2026-05-15T20:22:58.334Z" }, + { url = "https://files.pythonhosted.org/packages/a1/47/0728b986b8682d742ff68c16baa5af9d185484abfc635c5cc700f44e62be/numpy-2.4.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32f8f852273ef32b291201ac2a2c97629c4a1ee8632bb670e3443eaa09fc2e72", size = 15671157, upload-time = "2026-05-15T20:23:01.081Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0b/b905ae82d9419dc38123523862db64978ca2954b69609c3ae8fdaca1084c/numpy-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685681e956fc8dcb75adc6ff26694e1dfd738b24bd8d4696c51ca0110157f912", size = 16645703, upload-time = "2026-05-15T20:23:04.358Z" }, + { url = "https://files.pythonhosted.org/packages/5f/24/e27fc3f5236b4118ed9eed67111675f5c61a07ea333acec87c869c3b359d/numpy-2.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f64dd84b277a737eb59513f6b9bb6195bf41ab11941ef15b2562dbab43fa8ef", size = 17021018, upload-time = "2026-05-15T20:23:07.021Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a7/9041af38d527ab80a06a93570a77e29425b41507ad41f6acf5da78cfb4a4/numpy-2.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b42d9496f79e3a728192f05a42d86e36163217b7cdecb3813d0028a0aa6b72d7", size = 18368768, upload-time = "2026-05-15T20:23:09.44Z" }, + { url = "https://files.pythonhosted.org/packages/49/82/326a014442f32c2663434fd424d9298791f47f8a0f17585ad60519a5606e/numpy-2.4.5-cp312-cp312-win32.whl", hash = "sha256:86d980970f5110595ca14855768073b08585fc1acc36895de303e039e7dee4a5", size = 5962819, upload-time = "2026-05-15T20:23:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/cbf5d391b0b3a5e8cad264603e2fae256b0bde8ce43566b13b78faedc659/numpy-2.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:3333dba6a4e611d666f69e177ba8fe4140366ff681a5feb2374d3fd4fff3acb6", size = 12321621, upload-time = "2026-05-15T20:23:14.305Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d0/0f18909d9bc37a5f3f969fc737d2bb5df9f2ff295f71b467e6f52a0d6c4e/numpy-2.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:4593d197270b894efeb538dcbe227e4bcf1c77f88c4c6bf933ead812cfaa4453", size = 10221430, upload-time = "2026-05-15T20:23:16.887Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a4/fb50657c7cab297bf34edcd60a074cb0647f61771430d6363575274160fe/numpy-2.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ef248460b645c102026b82337cc4e88231909c66dd77b59ec6d6cac7e44f277", size = 16684760, upload-time = "2026-05-15T20:23:19.436Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/87e731299b9408eda705b3b9cb31c7bceb9347d2af9cbb16b2b1e4b5bc0f/numpy-2.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4603622bdcdbf8dccb1d9d5b21d16a7aa4e473ae6c8e14048d846fd4ca2907a0", size = 14694117, upload-time = "2026-05-15T20:23:21.832Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/0b2bb8acea222e9dd6e582afc2bc553b89b8833cbdccc68e68f050fb31f8/numpy-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6c18d49c67689c562854b53fdc433b93e47c12952aa6fa6d59f185e1a5992419", size = 5199141, upload-time = "2026-05-15T20:23:24.066Z" }, + { url = "https://files.pythonhosted.org/packages/39/60/b6972b5d47033d90000f0097c81a98b9486589a2d7003bf725bff275cb0d/numpy-2.4.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b1c663ddc641f4192e90511bec61a09bc231e3bbdb996cdc6edbcaa0e528d685", size = 6546954, upload-time = "2026-05-15T20:23:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e9/ed667cb12c11ca0adde431f685d3a5dd78e6f78b27228c581c8415198e9e/numpy-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93793222b524f692f12b2f8752ce8b1d9d9125b2bfd5dbf0fb69c92c5e1ce86c", size = 15669430, upload-time = "2026-05-15T20:23:28.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/e5/679f6ffeb01294b0008e5ada4a113cb47617bc0e1819a529fd7973c6d7f4/numpy-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1616bde34b2bcba2fa9bde06217ce00da4f3d1bdfb264d54525a99e8fe170d83", size = 16633390, upload-time = "2026-05-15T20:23:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/36/46/42bfffc9a780ec902ccd7470d3219192ee82b7b442710307dd85b4d121b0/numpy-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09d7d97da1c2c62f4818b3e150a57572ff8dcf1cf5ac501aac832ffd4ebd9566", size = 17020709, upload-time = "2026-05-15T20:23:34.08Z" }, + { url = "https://files.pythonhosted.org/packages/44/00/3e840bfee0cc6cec22209f2c97057f26eeb30de031e4933b4dfc0395416c/numpy-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d68d0b355ab2e39fe0de59001d7151dfdbbb880ef67baeed806661e03df5097", size = 18357818, upload-time = "2026-05-15T20:23:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/3447b400b9da84134575486f0f656541559b00d4b262477bce9b678bbca8/numpy-2.4.5-cp313-cp313-win32.whl", hash = "sha256:fe28b64777ddfa0eca9b5f51474034ebe3dcb8324f48f27b28f479085673ae33", size = 5961114, upload-time = "2026-05-15T20:23:39.586Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/a90d2220ffcdc0798f5d55bb5d5463cd6254ec9ef43f384dae80217d7a2f/numpy-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb4a6c9c537d6ccec9cc4aeae4261bd3cc79b070c67ddc0646f5b1c07fddde42", size = 12318553, upload-time = "2026-05-15T20:23:41.436Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/96f531fb3234545315152d34efdf3de7daee81254448447eb619e8d16967/numpy-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d7df2da2e7ea0624a43aa368104b3a3ce14aae98ad4bb2c9a93fecef76f1c97", size = 10222200, upload-time = "2026-05-15T20:23:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/a291caab5a3c520babf93ff77c54fd5fdb1ebbc3296cee2eb2146ce773b1/numpy-2.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2a235607a18df941760a695927051af4b1cd5d3ee85840d0e2af816785771feb", size = 14821438, upload-time = "2026-05-15T20:23:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/13dbb1159b864370568e7309063fd72667984df89db74e9caeb175d067c7/numpy-2.4.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:58dcf64969d870f36bc7fbd557d2617e997db7dc06261b6e3327148ea460d0a4", size = 5326663, upload-time = "2026-05-15T20:23:48.18Z" }, + { url = "https://files.pythonhosted.org/packages/7c/99/d233408072a0e019e2288e27edd23f7d572ccd4a73d1539baa3270ede85d/numpy-2.4.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:235f54b0156274d8fa3155db3ed6d2f401c7e8f3367c90db0a12f02a58fde6ed", size = 6646874, upload-time = "2026-05-15T20:23:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/c5/00/eeb6f193dfe767725e952e0464f3e51f44145c5dd261cd7389aa36ac0713/numpy-2.4.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3b5bb65437a3555c648e706475db01c645559ca80dc8b03e4f202ea757e0d6", size = 15728147, upload-time = "2026-05-15T20:23:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c9/b8ed039f1fde1b13a8807c893e7e2f9432a379f4d6401edecf0028da5b2c/numpy-2.4.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f09a7e5f017d7098c66522097c96257411c9620c0926212200d66bc8cee3976", size = 16681770, upload-time = "2026-05-15T20:23:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/11/5b/0198ef6cb7016eca6d895d392106012138127fab23f46637e76d5e25c9f5/numpy-2.4.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:993a88d8fdd8554466a8765cd8bacd97ba56b70ca6b0a04bcdca77f5afed4222", size = 17086218, upload-time = "2026-05-15T20:23:56.646Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fe/8821f3cfc660ae84c92ee158505941874b62c56a42e035a41425228cd8cf/numpy-2.4.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:84f58bed609b5669f5ad3d597901a4f1f86ee5b3c3708aaa55f05b4fe6e0f656", size = 18403542, upload-time = "2026-05-15T20:23:59.173Z" }, + { url = "https://files.pythonhosted.org/packages/0e/00/e64ecaf498865e7b091f57658b2c522503e5d1b70e43b807f5f8247e1d88/numpy-2.4.5-cp313-cp313t-win32.whl", hash = "sha256:7200c58f3f933ca61e66346667dcc8510bb111995e9ce15398a731e6a4afa4bb", size = 6084903, upload-time = "2026-05-15T20:24:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/20/c0/354997dedaf74e8311c2cf9a6027b476fd8d424cb92189cc0ae2b25f501c/numpy-2.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c26c71080d35db5002102f5d9ff614d45de02aa1f7802943e691e063e5ee93bc", size = 12458420, upload-time = "2026-05-15T20:24:03.735Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/917ee5ea4a31ca1a6e4c9a85386477efa318dcc60db257c5ef4adda096c1/numpy-2.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:2caa576d1707b275cba1aeb60a5c50daa6fa2a3f28ecb08123bc05fd439005db", size = 10291826, upload-time = "2026-05-15T20:24:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c1/3be0bf102fc17cff5bd142e3be0bfffabec6fa46da0a462396c76b0765d0/numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61", size = 16683455, upload-time = "2026-05-15T20:24:08.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3e/0742d724901fa36bc54b338c6e62e463a7601180da896aa44978f0adf004/numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77", size = 14704577, upload-time = "2026-05-15T20:24:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361", size = 5209756, upload-time = "2026-05-15T20:24:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/52/c0/23fb1bc506f774e03db66219a2830e720f4d3dbcaaddf855a7ff7bb6d96f/numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29", size = 6543937, upload-time = "2026-05-15T20:24:16.223Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/db4662c26e68520afcc84d672a6f9f5294063dee0e57a46d61afdaa7f9ed/numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce", size = 15685292, upload-time = "2026-05-15T20:24:17.978Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a", size = 16638528, upload-time = "2026-05-15T20:24:20.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/81/364388600932618fe735d97fdd2437cb8dd87a23377ac11d8b9d5db098b7/numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23", size = 17036709, upload-time = "2026-05-15T20:24:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/32/4a/a1185b18a94a6d9587e54b437e7d0ba36ecf6e614f1bea03f5249912c64e/numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25", size = 18363254, upload-time = "2026-05-15T20:24:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/95c1d2ed15ae97750ede8c8a0ac487c9c01207afff430f47078b1d9d7dc5/numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080", size = 6010184, upload-time = "2026-05-15T20:24:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f", size = 12450344, upload-time = "2026-05-15T20:24:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/c0ae481f7c3b2f85869bcd8fc5d30aa7c96b394162eef9c9315957f115c5/numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957", size = 10495674, upload-time = "2026-05-15T20:24:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/57/89/c5a4c677acf17aa50ba09a15e61812f90baac42bb6ca38d112e005858351/numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3", size = 14824078, upload-time = "2026-05-15T20:24:34.669Z" }, + { url = "https://files.pythonhosted.org/packages/e7/52/57e7144284f6b51ba93523e495ff239260b1ecd5257e3700a436332e5688/numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be", size = 5329246, upload-time = "2026-05-15T20:24:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b3/09dbce80fd4a7db4318f2fc01eec0ae76f29306442b5a32d4b811d082cdf/numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7", size = 6649877, upload-time = "2026-05-15T20:24:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/dbdb23e82d540b757690ef13f011c386fca6a63848eec6136baf8ce7cbed/numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40", size = 15730534, upload-time = "2026-05-15T20:24:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bd/68f6e9b3c20decf40ac06708a7b506757e3a8588efed32988d1b747316be/numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3", size = 16679741, upload-time = "2026-05-15T20:24:44.874Z" }, + { url = "https://files.pythonhosted.org/packages/39/1d/0fcac0b6b4ea1b50ca8fca05a34bed5c8d56e34c1cb5ffb04cf76109ac3c/numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c", size = 17085598, upload-time = "2026-05-15T20:24:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/a472b2564cf6cc498ad7aa9741d9832648221b8ab8cc0dbef41faa248ede/numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963", size = 18403855, upload-time = "2026-05-15T20:24:50.474Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a4/da82196f8cc4bd28ecf17bd57008c84f3d4696caf06753d9bad45e4ad749/numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5", size = 6156900, upload-time = "2026-05-15T20:24:53.134Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/860959b91a73d9a085006554fa3850da51a7ffab64599bac5097243438ab/numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a", size = 12638906, upload-time = "2026-05-15T20:24:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2a/bbd3097913083ad07c0f28fc9629666221fc18923e17ce97ae22a5dccdd6/numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b", size = 10565875, upload-time = "2026-05-15T20:24:57.425Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5d/9a644cfb841bc76b584afc3af1708b3bf6c5cb51fc84a7008246cd93b7b7/numpy-2.4.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6bf0bfc1c2e1db972e30b6cd3d4861f477f3af908b27799b239dc3cbe3eb4b95", size = 16847544, upload-time = "2026-05-15T20:24:59.746Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/4fe5e3ba76d858dae1fe79078818c0520447335be0082c0dedf82719cc08/numpy-2.4.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:73d664413fb97229149c4711ef56531a6fe8c15c1c2626b0bbe497b84c287e70", size = 14889039, upload-time = "2026-05-15T20:25:03.179Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6f/79f195abf922ecc43e7d0eb6cc969462a71b524a35bcd1fa26b4a1d7406a/numpy-2.4.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:b35bee5ef99e8d227a07829bee2e864fcb65f7c157646fcd8ec8b4b45dd8b88f", size = 5394106, upload-time = "2026-05-15T20:25:05.659Z" }, + { url = "https://files.pythonhosted.org/packages/58/6f/79cd6247205802bcbd10b40ea087e20ded526e10e9be224d34de832b216e/numpy-2.4.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:02981d0fc9f9ce147643d552966d47f329a02f7ecb3b113e84207242f20dfa83", size = 6708718, upload-time = "2026-05-15T20:25:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/d7/22/5f378a9d4633c98f28c4709d4144b1a4630c5c09e109d2e781e2d26c8fe1/numpy-2.4.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e63caf31a1df06338ae63d999f7a33a675ced62eea9c9b02db4b1c1f45cff38", size = 15798292, upload-time = "2026-05-15T20:25:10.689Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/cec582febef798c99888892d92dc1d28dfe29cb427c41f44d13d0dec208f/numpy-2.4.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fc52b85a7b45e474be53eddf08e006d22e381a4e41bcde8e4aa08da0e7d198", size = 16747406, upload-time = "2026-05-15T20:25:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dc/d358a16a6fec86cf736b8fbe67386044b3fa2aded1a80cff90e836799301/numpy-2.4.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:40c71d50a4da1a7c317af419461052d3911a5770bfc5fd55baf52cc45e7a2c20", size = 12504085, upload-time = "2026-05-15T20:25:16.667Z" }, ] [[package]] @@ -3222,7 +3718,7 @@ dependencies = [ { name = "colorlog" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "sqlalchemy" }, @@ -3235,11 +3731,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -3248,7 +3744,8 @@ version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -3317,12 +3814,15 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.0" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3331,68 +3831,68 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, - { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, - { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, - { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, - { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, - { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, - { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, - { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, - { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, - { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, - { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, - { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, - { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, - { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, + { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -3402,7 +3902,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", 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 = [ @@ -3414,7 +3914,8 @@ name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ @@ -3527,12 +4028,15 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.0" +version = "12.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3541,98 +4045,98 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, - { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, - { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, - { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, - { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, - { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, - { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, - { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, - { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, - { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, - { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, - { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, - { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, - { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, - { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, - { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, - { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, - { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, - { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, - { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, - { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] @@ -3640,7 +4144,8 @@ name = "platformdirs" version = "4.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ @@ -3649,12 +4154,15 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3663,22 +4171,22 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] name = "plotly" -version = "6.5.2" +version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/7f/0f100df1172aadf88a929a9dbb902656b0880ba4b960fe5224867159d8f4/plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6", size = 6911286, upload-time = "2026-04-09T20:36:45.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0", size = 9898444, upload-time = "2026-04-09T20:36:39.812Z" }, ] [[package]] @@ -3713,7 +4221,8 @@ name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ @@ -3725,9 +4234,12 @@ name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3752,11 +4264,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +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/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { 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]] @@ -3764,7 +4276,8 @@ name = "pymoo" version = "0.6.1.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "alive-progress", marker = "python_full_version < '3.10'" }, @@ -3810,9 +4323,12 @@ name = "pymoo" version = "0.6.1.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3826,12 +4342,12 @@ dependencies = [ { name = "autograd", marker = "python_full_version >= '3.10'" }, { name = "cma", marker = "python_full_version >= '3.10'" }, { name = "deprecated", marker = "python_full_version >= '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "moocore", marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", 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.10.*'" }, - { name = "scipy", version = "1.17.0", 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/ad/6c/5637688bb0e484ad7cd84e9f24f575d1b3c4ef28ce0974836bce7660106a/pymoo-0.6.1.6.tar.gz", hash = "sha256:d48077c7b612b149e7db5351459bf96a0950e84ebcd5b7b953bf46b3dcf1ac28", size = 1216128, upload-time = "2025-11-25T03:18:30.651Z" } wheels = [ @@ -3895,7 +4411,8 @@ name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, @@ -3913,12 +4430,15 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -3936,9 +4456,9 @@ dependencies = [ { name = "pygments", marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -3948,7 +4468,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "py-cpuinfo" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } wheels = [ @@ -3957,18 +4477,18 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "coverage", version = "7.14.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "pluggy" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -3978,7 +4498,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ @@ -4043,11 +4563,11 @@ wheels = [ [[package]] name = "pytz" -version = "2025.2" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] @@ -4151,23 +4671,58 @@ wheels = [ name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ @@ -4194,16 +4749,16 @@ wheels = [ [[package]] name = "rich" -version = "14.3.2" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -4220,7 +4775,8 @@ name = "scikit-bio" version = "0.7.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "array-api-compat", version = "1.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -4231,7 +4787,7 @@ dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "patsy", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "statsmodels", marker = "python_full_version < '3.10'" }, ] @@ -4266,12 +4822,15 @@ wheels = [ [[package]] name = "scikit-bio" -version = "0.7.1.post1" +version = "0.7.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -4281,52 +4840,52 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "array-api-compat", version = "1.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "array-api-compat", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "biom-format", marker = "python_full_version >= '3.10'" }, { name = "decorator", marker = "python_full_version >= '3.10'" }, - { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h5py", version = "3.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "natsort", marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "patsy", marker = "python_full_version >= '3.10'" }, - { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.17.0", 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 = "statsmodels", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/d2/7c8d097db3f158cf2f346cc2586943ee3d85263177ddacc7d663d9f95131/scikit_bio-0.7.1.post1.tar.gz", hash = "sha256:cbd92418b711492837ea5ca3a088b540e725bea53a45bc2332b2631afd539f95", size = 5665966, upload-time = "2025-10-30T19:19:43.042Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/38/5fda6a940bb367ca083acaf393cf4f8d4ef9b9fee59a543db7e84af4dfcc/scikit_bio-0.7.1.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68d4bd49c405d1cda4337899b620027ca6090d011ca900576df70ce97d5febfa", size = 6528024, upload-time = "2025-10-30T19:18:50.83Z" }, - { url = "https://files.pythonhosted.org/packages/21/c4/43dc66de1d6699014238928eae7d183e27e5f0838dbaa917711cae95526b/scikit_bio-0.7.1.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b141af25dd831d729a99dcfbf9410bf395bb4f2a10bd1f8d33390383bba6d3e", size = 6533109, upload-time = "2025-10-30T19:18:52.921Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e7/c0c3a2b93ecc8a076ec0b526a8596b14d8c97d9d669968ba27e0eb2738a9/scikit_bio-0.7.1.post1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6294f4d24cd5916f45a99291ca197497ff63e3068759a5d05b9141135c760d", size = 10774670, upload-time = "2025-10-30T19:18:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/ce/84/618d9eefeb933f0cb080e4d0ce6b561002f7967057a24e52278db067282f/scikit_bio-0.7.1.post1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:683fed27960fb805efc0a2560983035c6e8757a10c033a496c4db46c1f7d207e", size = 10800662, upload-time = "2025-10-30T19:18:57.827Z" }, - { url = "https://files.pythonhosted.org/packages/28/44/570eb1a2bda69f03255b4129521ba4e5e045b590855fcd65dec269ef8ee4/scikit_bio-0.7.1.post1-cp310-cp310-win_amd64.whl", hash = "sha256:dd0073860b29561480fc2ef125019e349454497e18dce299d2cbbe93beccd7c6", size = 6447498, upload-time = "2025-10-30T19:18:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/e6/91/eb2a2eed239612ec22d69f948cd02a90a86ec0a3eb0ddf468c66e54a8609/scikit_bio-0.7.1.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d0593aa776942aaf27431835698b415d1e9f13cd8cd118f412b8ff4cbc5b8", size = 6537083, upload-time = "2025-10-30T19:19:02.16Z" }, - { url = "https://files.pythonhosted.org/packages/74/c2/de147eef13bfcc19cb086061ef52a6dcb33613b535c5b8b903207a18b1f2/scikit_bio-0.7.1.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22c9d1d48fc037afe2bde68c7db479df177564a17781377e0e0c4a613b5283b9", size = 6535208, upload-time = "2025-10-30T19:19:03.481Z" }, - { url = "https://files.pythonhosted.org/packages/72/2b/68f1ef336a4285b5898a2910d65ebc3f329035228d0fdabce0f1775402b4/scikit_bio-0.7.1.post1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b78d6a0b52f0acc4a28aa3a5a06b8846e147b8404d84d08e18125828f77a55e", size = 11067343, upload-time = "2025-10-30T19:19:04.995Z" }, - { url = "https://files.pythonhosted.org/packages/5c/35/b100d2e6d5e0fd4de4af4c3be9e70c291702afb314b549374331df4f99e4/scikit_bio-0.7.1.post1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddf2f40b99eea14ecd3196e27e1d9f0b7071856edf8782e1b31fcd39b28c8112", size = 11085265, upload-time = "2025-10-30T19:19:07.205Z" }, - { url = "https://files.pythonhosted.org/packages/5c/60/9896f3378ab768af4eb56749bca7eb229be1ef76f6417506fb2437330ed7/scikit_bio-0.7.1.post1-cp311-cp311-win_amd64.whl", hash = "sha256:4689b8e47ecd235c71a883c1cbcaad48b51c8c2676fd6d11d4e4134e1bf675fb", size = 6448277, upload-time = "2025-10-30T19:19:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9e/1709b0eb124cddba864b855df6c428e96c224d99ffb5d008329a5d8caec4/scikit_bio-0.7.1.post1-cp311-cp311-win_arm64.whl", hash = "sha256:593186762927e6248bba3fa20e2656be61c1e3210b0ddefe1428d4e8e01b80fd", size = 6342074, upload-time = "2025-10-30T19:19:10.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4e/df25894303407646477823d37409b88e9589a64432bcfd97b5c69e290f01/scikit_bio-0.7.1.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b445ed12f486d06fab36c86d1c858b4baa3978c33b7a4f76d967f6ffb7e1375", size = 6539786, upload-time = "2025-10-30T19:19:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e4/f35635cdf167a0335cae98d6b89da08de4f89c80f740304ba642030761ad/scikit_bio-0.7.1.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0373e14eb37403cfb64de65a047ac11b91b8d83534e98fe9e4404c3d387a25e9", size = 6544189, upload-time = "2025-10-30T19:19:13.261Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5a/25e6fa4db4e0cdbaecf395af06f8d95830ad1e57b0daf176bebe1c957ad6/scikit_bio-0.7.1.post1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd873c615777ab28c448921fc98639897c37ea58412fd704cfe1a96b9bedd0f6", size = 10887784, upload-time = "2025-10-30T19:19:14.829Z" }, - { url = "https://files.pythonhosted.org/packages/78/fd/45a25d9c4ff917582f951f08c2f06ab719802f3d4f9a6dcb4fc291c763f7/scikit_bio-0.7.1.post1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb724ab26b393a074cbb5e9bb47277284a1dc68e70684147a6d0a3ea2d822cdd", size = 10946298, upload-time = "2025-10-30T19:19:17.003Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cd/cea3b6fbe18cd94226e38196e71b4bdb57b187722ade8df63b38a0c15560/scikit_bio-0.7.1.post1-cp312-cp312-win_amd64.whl", hash = "sha256:5e93c64077e815752d7c27b5117ac498b3847a4cf3332aa71a8fcf601ed0fe86", size = 6459262, upload-time = "2025-10-30T19:19:19.349Z" }, - { url = "https://files.pythonhosted.org/packages/d9/61/8dcdd863634f77db5dac566cdd4fe09a01786e42f0ccf06d32e91f68f5a8/scikit_bio-0.7.1.post1-cp312-cp312-win_arm64.whl", hash = "sha256:d86eaeb7b1e101d1d8fa79de2a23ef76697e2252f945884f5ac08f829f0fe1f9", size = 6343827, upload-time = "2025-10-30T19:19:20.77Z" }, - { url = "https://files.pythonhosted.org/packages/fd/96/a8c8bd5cddbc0159f67b8efe399b4f786887608292edd90e57b669dfb58c/scikit_bio-0.7.1.post1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8269b297ade34ed58939c1f8d5b9506071d2160ce83277f29b938fdad29fbc40", size = 6532159, upload-time = "2025-10-30T19:19:22.223Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e3/679a58a84b5c863b27ba130f3691083f6bb71c3f3142a20a1ebce5f3988f/scikit_bio-0.7.1.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d478ed7858e33d7af9a4bf9c267d46e46eae8194c8ed83fc62f23607bf660f1", size = 6536898, upload-time = "2025-10-30T19:19:24.037Z" }, - { url = "https://files.pythonhosted.org/packages/e7/65/c776d8d32e69d2074bc2eb8c46c7807e1ac572548da9fe950eac249ede4f/scikit_bio-0.7.1.post1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bccf748c80dc9779ed93522513c842473de460e58e76bfa128ddf3cfdea2f604", size = 10848935, upload-time = "2025-10-30T19:19:26.074Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e0/fc497c545850e2e3683e8a30139dccfff0616f02658dfbd6b5f92bf2499d/scikit_bio-0.7.1.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ebdd76585f65a3e28f90833c3e4f5fc8241b36c1378bc65abad3330142fb504", size = 10899404, upload-time = "2025-10-30T19:19:27.821Z" }, - { url = "https://files.pythonhosted.org/packages/71/5c/fb855a6f767c88c82bb28ec125457ea8d654b3187aea49bd2696df923513/scikit_bio-0.7.1.post1-cp313-cp313-win_amd64.whl", hash = "sha256:9ec664c8a9da363941570e0a2fc7813d57530b7de2ea1dba71487daaef3be9af", size = 6456865, upload-time = "2025-10-30T19:19:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/84/3d/f7549b21f314b2a23f5a6148e0a17c88d8170bd1599ae8c735f492a01ce5/scikit_bio-0.7.1.post1-cp313-cp313-win_arm64.whl", hash = "sha256:d8890f3f39d12f2fc2cbce2c8b84081d6ccdadf87c870b80e5bf45826b7262b8", size = 6342824, upload-time = "2025-10-30T19:19:31.184Z" }, - { url = "https://files.pythonhosted.org/packages/29/7f/11d1c5e10001870e42f91e0236d6bb3fabd1bd0f6d43eb149fe74596e951/scikit_bio-0.7.1.post1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1bdb3279fa601edc31ecd0b9635ec7dfac594a441124fffafca5abaa3b68ad6c", size = 6530286, upload-time = "2025-10-30T19:19:32.857Z" }, - { url = "https://files.pythonhosted.org/packages/6b/73/60707444c38f8a791046d16c0f54dc4fa98e4d34c01e38e306799a2f0754/scikit_bio-0.7.1.post1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bbd0f0d7928e5f65774b3b63d5581f813989d2f900892864d49aed24cbf6f265", size = 6543599, upload-time = "2025-10-30T19:19:34.188Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e4/5a06cce37472df5503192435637a51ee3a658e4ca7a7c2671f0a4f32737d/scikit_bio-0.7.1.post1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4eca29267f503d9900350013f786d04710f81d0f3254e4a7b6121892ddd202", size = 10855453, upload-time = "2025-10-30T19:19:35.678Z" }, - { url = "https://files.pythonhosted.org/packages/d8/6d/5700bb155429a6aa74ad8ef94a905f52c901fd6de2c7d03b99222e77f1bf/scikit_bio-0.7.1.post1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:286b1f9d39cd23253bc94b7e510bfabc993dcf8be8062b3426afd14e3e0756cc", size = 10849124, upload-time = "2025-10-30T19:19:37.452Z" }, - { url = "https://files.pythonhosted.org/packages/f4/39/05c6f52c53f16b00003099db1ad9b51f037a90382fe7e31c96376896efdc/scikit_bio-0.7.1.post1-cp314-cp314-win_amd64.whl", hash = "sha256:58b7030cfebd3af7444d8f7c1dd2184d296c5f94ba0163ca3e4b62f3e8dd0035", size = 6453898, upload-time = "2025-10-30T19:19:39.581Z" }, - { url = "https://files.pythonhosted.org/packages/ff/95/ff64d260b2dd308338842287b0a165eb3b97e123ead5ff5206862475f812/scikit_bio-0.7.1.post1-cp314-cp314-win_arm64.whl", hash = "sha256:56e63b87fc01f72f247de616069ecd1142cfca5e1c10c2f72611703981841c41", size = 6342299, upload-time = "2025-10-30T19:19:41.73Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/5a/6f/6bb9eda0faccafede38979b79d161f81bb3cbf56ec81dcd7c26fbdadbc4b/scikit_bio-0.7.2.tar.gz", hash = "sha256:d0a991818882c88ca47d9f6441321a440439e1a8a6716a101a931d590c395fe8", size = 5667396, upload-time = "2026-02-12T00:24:30.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/63/ca3236b8c4b7fe638ada64f76a2853343a4296ada72ac2024f3939bd882d/scikit_bio-0.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:275f2d4a68d7a8e8567e2228c352d171672fb00ea8dc1c9706138b1b3ebfa462", size = 6465138, upload-time = "2026-02-12T00:23:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/de76d17868d23b8e88342d307425c53ccefd2548b3bb974de0bdab89262f/scikit_bio-0.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc7f8811ed236ac33da0c2d542eb601b6a8dc9e13699295ad2eef5b0a376455", size = 6437564, upload-time = "2026-02-12T00:23:34.996Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/5a2dfc7439c00b4f0b50f4c1abb7557128e11883e1b59538744cf49283e3/scikit_bio-0.7.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e756355e1a4745206370690b8182b070642e731a54a5d4d94f86a13c3da8b81a", size = 9830284, upload-time = "2026-02-12T00:23:37.157Z" }, + { url = "https://files.pythonhosted.org/packages/1e/62/3ceef25e854227dcda3dff4df6841a9759bb35f79aee9f8c6b441fb45f57/scikit_bio-0.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2dacce1c86e16378a330d22bb57c42398902f3304033e82009e59d8a9ae1c18", size = 9830204, upload-time = "2026-02-12T00:23:39.43Z" }, + { url = "https://files.pythonhosted.org/packages/8d/81/ec37fce9fdba200bfdbd5782913ec658e83d11b1127557aaec4a0cdd30f7/scikit_bio-0.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:818560d53c2bb3c9d1b6961de3c3884cebcbfee76293b987c0d0f669354c0907", size = 6388666, upload-time = "2026-02-12T00:23:41.524Z" }, + { url = "https://files.pythonhosted.org/packages/36/20/8162865e96b3195ce721379892dc4ec5f81ee4c2c29a68571b7e6a05957c/scikit_bio-0.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a85cb097e755ee9e63a29a1e560fb5bdd9660794fac844d060cf92ae12c864f8", size = 6462098, upload-time = "2026-02-12T00:23:43.138Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6c/7af46e94698bfcdfbd4eabb261e70a8cb2eb69ebed27804e30693082db5a/scikit_bio-0.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709b1f9f5eb8aeeceba4c99663cea79a8ca4e8d4fcabcfeae97c026a50a3322e", size = 6432705, upload-time = "2026-02-12T00:23:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/4b/35/88e97e2d96106be663d1ff10e1c74869bf7972510daa399a8d4c644ab530/scikit_bio-0.7.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f53f1c6871eafab6c5fb831e3a38f3a1dcf2f38eca50a9f9cce9b8abf19620bc", size = 10017556, upload-time = "2026-02-12T00:23:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/de/51/0fa75940de0fe3e0d7a34828831efb6cb3d673c2b23d795da5c74b5cdc7f/scikit_bio-0.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8cd11c45fce6c5c49512ee08787702eed9c0fb97d7cbd0bbf2d09fe2ddcb49c", size = 10021725, upload-time = "2026-02-12T00:23:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/db/63/d3010fdc1486898511b05c9250c96e87a28bb3e584e05c0e0f6e9b4766ed/scikit_bio-0.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:a2751e34c02cba91a434929dd2a3dec1a75f5eb788c3e81c141983063d5af38c", size = 6388577, upload-time = "2026-02-12T00:23:50.567Z" }, + { url = "https://files.pythonhosted.org/packages/83/4d/9a420e15f1554cb983a6d7bde19db74247a29b4bc2f52eb28e9e698e28b5/scikit_bio-0.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:704167b3da4b3677de195ab9cfe4f6888ef7b2a4d25d4d0317162aea79f09476", size = 6285531, upload-time = "2026-02-12T00:23:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3e/533150782121da572c422c7659ead6688deea251900af4072fe8e73628a2/scikit_bio-0.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc5ab62d9a41df60ec4e45d7b0298412cd51d19b05be27c4515c2ac2c427ae88", size = 6471768, upload-time = "2026-02-12T00:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/e5/82/068e77d31093a469a44b58a74683c2da7d62f244ce3580bcf09aaeb3f3bb/scikit_bio-0.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9aca4ada9773d227ccff6181f1683993188114a6282abac267404095c9e599f2", size = 6438077, upload-time = "2026-02-12T00:23:55.632Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/74b3b3fef3cec61db3d8467be6e27d079cc8542a700e918c50fa7520c091/scikit_bio-0.7.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9ea5b4ce0047247b9f40bdf0ab2c199f4ac52a8641082071cef67b7f8934ce1", size = 10042987, upload-time = "2026-02-12T00:23:57.081Z" }, + { url = "https://files.pythonhosted.org/packages/6e/68/7a70e65189ea9b4524fe98578ecd38d8c772c191a600a03cb2da5368267f/scikit_bio-0.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07a9b07cdd6311b4eeb7144f263f632d1450408ae057ea66d5631b727bd76798", size = 10101809, upload-time = "2026-02-12T00:23:58.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/ecd5135713cd41ac4ddfebc5f6b381d8075322617f850e92f892d79b31c5/scikit_bio-0.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:b832e087462eb06b802c952da133fc69ab3525448ec7a0b1fdb8535335af1148", size = 6397219, upload-time = "2026-02-12T00:24:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/14/fa/5fca7b5c595f4bc2e38f459917d334e245f5bb9ae694ac7396790898605b/scikit_bio-0.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:ed0bf1ba6a46391a738796591686ca474157a7586a766083bab852793dc02c82", size = 6285552, upload-time = "2026-02-12T00:24:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/3c/53/9c3dc4b736f945be852ae43474a901c06763bba3a5cf52f2adbfcba42bff/scikit_bio-0.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:367bdacd55d38cc7838efc3a76ad6a282e7854a9361ddccdb87dcde4e5697bb6", size = 6466840, upload-time = "2026-02-12T00:24:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0e/25373b3e62fee797f517edde44d69e7fe99ff8acf6a1faf05839885842c1/scikit_bio-0.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6c5e223277f1b2f5c25ffe9639c76238b21976a11dd7b0c9473c3ac4df733eb", size = 6433503, upload-time = "2026-02-12T00:24:05.536Z" }, + { url = "https://files.pythonhosted.org/packages/55/00/08d301ee1f091768928e2ec0371aefb4b67f70f718f93e23d145a23353a1/scikit_bio-0.7.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8186a8822374e43826186e55e9db03b51662230075f6a2d48b31c8707fe68e93", size = 9986111, upload-time = "2026-02-12T00:24:07.402Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d2/398ae8185cd8fd59d099128fbb8481f58b596dd6ab29d5dcd14023561e7f/scikit_bio-0.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ced10fd1fb03e1ae99c6d897c320af05e109dc94956ebd45020af167ff2e129", size = 10047817, upload-time = "2026-02-12T00:24:09.409Z" }, + { url = "https://files.pythonhosted.org/packages/e6/20/3c9cbcbe91f1bd881d441da82570857bf302a3987f065ba69b6de03e61fc/scikit_bio-0.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:55a5dd9821e5260764eff392b9e040cded1dccec2b55c3c7c69e7b7d1d73caa9", size = 6394708, upload-time = "2026-02-12T00:24:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e4/628c154fbf83855f9157411bcb80de0b65902ef942318643ff16746fb5aa/scikit_bio-0.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:d5a4838f43bc28b526a9043057678b686f6ac7498ea204c77c4ad26984639fcc", size = 6284476, upload-time = "2026-02-12T00:24:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/62/87/ffc5384a6793ed20f0fd550c6c92cac4eec85763d51d5a1d0e6bb76e37c6/scikit_bio-0.7.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0d44464cfa8ba866d4c839f571a353358cce4f9092961e84e66ca84b4cdb2641", size = 6490423, upload-time = "2026-02-12T00:24:14.128Z" }, + { url = "https://files.pythonhosted.org/packages/05/c6/7b4de57d21e1587df1a9b8a599bb09ed36135a9cbfb57bcce1e695c15c20/scikit_bio-0.7.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ef4115e2b89f2bdf8005eb580336ba6c9b8ae611018b51f01f60c9d6c3f2133d", size = 6460201, upload-time = "2026-02-12T00:24:16.975Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f7/94fed4818f0b616845d89be2ac7149d5afe3ec61df15e77f04811a2b84df/scikit_bio-0.7.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a631f2695980ce35b38c5e8671e70ef9da84e5c115a47297a1ef6016ecd3095", size = 9971402, upload-time = "2026-02-12T00:24:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/da/be/4eeea67c506c563e59e2e472563d9ddbd8bde8ef8c77e0fe326f161c6d36/scikit_bio-0.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54eb5097ebd79821fba548df9df1593dcc1a1499a5630305f8c9e654e741dc26", size = 9998447, upload-time = "2026-02-12T00:24:21.966Z" }, + { url = "https://files.pythonhosted.org/packages/27/c2/01b9072ffac75312902b6007c546c5713fe1084bde27a7479c32bce542dc/scikit_bio-0.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:0e57e93c18309a5b1695186faec40b93d299a5bc85cbad338db90189f142de41", size = 6416146, upload-time = "2026-02-12T00:24:26.479Z" }, + { url = "https://files.pythonhosted.org/packages/46/23/c95e4d0a65fbbf859e40cce4f221291d1d3ddb1ebe03df1b5c0e0c0ed642/scikit_bio-0.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:d938d40fbffc2f486eab7a56487bfdfd5defa3484dcb43e44aea8e7bceececf4", size = 6306354, upload-time = "2026-02-12T00:24:28.869Z" }, ] [[package]] @@ -4334,7 +4893,8 @@ name = "scipy" version = "1.13.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -4428,12 +4988,15 @@ wheels = [ [[package]] name = "scipy" -version = "1.17.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -4442,70 +5005,70 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, - { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, - { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, - { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, - { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, - { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, - { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, - { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, - { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, - { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, - { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, - { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, - { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, - { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, - { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, - { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, - { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, - { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, - { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, - { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, - { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, - { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, - { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, - { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, - { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, - { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, - { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, - { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, - { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, - { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, - { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, + { name = "numpy", version = "2.4.5", 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" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] [[package]] @@ -4514,12 +5077,12 @@ version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] 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 = [ @@ -4531,10 +5094,12 @@ name = "secretstorage" version = "3.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ - { name = "cryptography", marker = "python_full_version < '3.10'" }, + { name = "cryptography", version = "47.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.9'" }, + { name = "cryptography", version = "48.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version > '3.9' and python_full_version < '3.10'" }, { name = "jeepney", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } @@ -4547,13 +5112,14 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.10.*'", ] dependencies = [ - { name = "cryptography", marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography", version = "48.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, { name = "jeepney", marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } @@ -4563,11 +5129,11 @@ wheels = [ [[package]] name = "setuptools" -version = "82.0.0" +version = "82.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] [[package]] @@ -4593,19 +5159,20 @@ name = "sphinx" version = "7.4.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", ] dependencies = [ { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "babel", marker = "python_full_version < '3.10'" }, { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "imagesize", marker = "python_full_version < '3.10'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "imagesize", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, @@ -4632,11 +5199,11 @@ dependencies = [ { name = "babel", marker = "python_full_version == '3.10.*'" }, { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "imagesize", marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "packaging", marker = "python_full_version == '3.10.*'" }, { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "requests", marker = "python_full_version == '3.10.*'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, @@ -4665,11 +5232,11 @@ dependencies = [ { name = "babel", marker = "python_full_version == '3.11.*'" }, { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "jinja2", marker = "python_full_version == '3.11.*'" }, { name = "packaging", marker = "python_full_version == '3.11.*'" }, { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, @@ -4689,9 +5256,12 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", @@ -4701,11 +5271,11 @@ dependencies = [ { name = "babel", marker = "python_full_version >= '3.12'" }, { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "packaging", marker = "python_full_version >= '3.12'" }, { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, @@ -4809,70 +5379,70 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.49" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", version = "3.2.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, - { name = "greenlet", version = "3.3.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, - { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, - { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/f1/69/c84f10a7fb0d6c50c0f6028cab1373ac1bc70a824d53bf857c33eddde5c4/sqlalchemy-2.0.48-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4599a95f9430ae0de82b52ff0d27304fe898c17cb5f4099f7438a51b9998ac77", size = 2160429, upload-time = "2026-03-02T15:44:11.019Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c8/2e0de4efcba76ae8cc84000bc0aedf45f7d2674a7d8cf66b884a03c3f310/sqlalchemy-2.0.48-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f27f9da0a7d22b9f981108fd4b62f8b5743423388915a563e651c20d06c1f457", size = 3236035, upload-time = "2026-03-02T16:01:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/0822c24212a2943b3df02a02c49b2b32ab67705eaa0d2f40f28f9c2e8084/sqlalchemy-2.0.48-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fcccbbc0c13c13702c471da398b8cd72ba740dca5859f148ae8e0e8e0d3e7e", size = 3235358, upload-time = "2026-03-02T16:07:58.002Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/f1c7c16d5ea0e4fbc14b473f02daedef8d77c582ef3c18b30b7307f85cff/sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a5b429eb84339f9f05e06083f119ad814e6d85e27ecbdf9c551dfdbb128eaf8a", size = 3185479, upload-time = "2026-03-02T16:01:32.781Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b8/95cb9642e608d02a0fd96bb3f7571b20a081313a178e1e661cc5dba37472/sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bcb8ebbf2e2c36cfe01a94f2438012c6a9d494cf80f129d9753bcdf33bfc35a6", size = 3207488, upload-time = "2026-03-02T16:07:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/24/cd/0dda04e28df0db4ed0b7d374f7eb7da8566db523dbac9f627cc6e0422c6d/sqlalchemy-2.0.48-cp39-cp39-win32.whl", hash = "sha256:e214d546c8ecb5fc22d6e6011746082abf13a9cf46eefb45769c7b31407c97b5", size = 2119494, upload-time = "2026-03-02T15:50:24.983Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1d/a98057e05608316cd3c2710f0b3d35e83cec6bdf00833b53a02235a1712f/sqlalchemy-2.0.48-cp39-cp39-win_amd64.whl", hash = "sha256:b8fc3454b4f3bd0a368001d0e968852dad45a873f8b4babd41bc302ec851a099", size = 2142903, upload-time = "2026-03-02T15:50:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/1d/64/6eb36149b96796ecbc1e2438959d08475e1f8765acbe007f4785a603c39c/sqlalchemy-2.0.49-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43d044780732d9e0381ac8d5316f95d7f02ef04d6e4ef6dc82379f09795d993f", size = 2162373, upload-time = "2026-04-03T16:49:49.55Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/87e57cfa06af0032a7470660d33e93ad0a2480781bb7705f4312471b993e/sqlalchemy-2.0.49-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d6be30b2a75362325176c036d7fb8d19e8846c77e87683ffaa8177b35135613", size = 3237991, upload-time = "2026-04-03T17:04:07.027Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/0099d0d554313c3587155b60288a9900660afc9989bf382176a5f4d7531b/sqlalchemy-2.0.49-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d898cc2c76c135ef65517f4ddd7a3512fb41f23087b0650efb3418b8389a3cd1", size = 3237313, upload-time = "2026-04-03T17:09:53.187Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9b/a61fcb2e8439a2282e4ac0086bb613e88cd18168cddb358fa2c5790d4705/sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:059d7151fff513c53a4638da8778be7fce81a0c4854c7348ebd0c4078ddf28fe", size = 3187435, upload-time = "2026-04-03T17:04:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/2165d3f8fa593f20039505af15474f63e85ffd7998afb6218b0fc0cd98e0/sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:334edbcff10514ad1d66e3a70b339c0a29886394892490119dbb669627b17717", size = 3209446, upload-time = "2026-04-03T17:09:55.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/8d/9630ddc9a4db638a7f29954b9e667a4ece41ff65e117460473ca41f06945/sqlalchemy-2.0.49-cp39-cp39-win32.whl", hash = "sha256:74ab4ee7794d7ed1b0c37e7333640e0f0a626fc7b398c07a7aef52f484fddde3", size = 2121680, upload-time = "2026-04-03T16:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5c/480f5d8c737cfb4a494f87de6e0e58a6b6346a0f4db1fa8122c89828e32d/sqlalchemy-2.0.49-cp39-cp39-win_amd64.whl", hash = "sha256:88690f4e1f0fbf5339bedbb127e240fec1fd3070e9934c0b7bef83432f779d2f", size = 2144917, upload-time = "2026-04-03T16:55:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, ] [[package]] @@ -4882,14 +5452,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "patsy" }, { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.17.0", 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 = [ @@ -4932,56 +5502,56 @@ wheels = [ [[package]] name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +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/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { 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]] @@ -5002,15 +5572,17 @@ version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "id" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, { name = "packaging" }, { name = "readme-renderer" }, - { name = "requests" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "requests-toolbelt" }, { name = "rfc3986" }, { name = "rich" }, - { name = "urllib3" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } wheels = [ @@ -5028,32 +5600,60 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "werkzeug" -version = "3.1.5" +version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] @@ -5155,9 +5755,9 @@ wheels = [ [[package]] name = "zipp" -version = "3.23.0" +version = "3.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, ] From 2db696c5b7ccb5aa320c1b869eab007b558ffd92 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 06:43:48 +0800 Subject: [PATCH 15/29] Drop stale OMP_NUM_THREADS from cibuildwheel config The threadpool is now the default parallelism backend (USE_OPENMP=OFF, USE_THREADPOOL=ON), so the OpenMP env var has no effect. Removed from both CIBW_ENVIRONMENT in wheels.yml and the [tool.cibuildwheel.environment] block in pyproject.toml. --- .github/workflows/wheels.yml | 3 +-- pyproject.toml | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 11a2be8..4f5a79a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -84,8 +84,7 @@ jobs: # Environment variables for builds CIBW_ENVIRONMENT: > CMAKE_BUILD_PARALLEL_LEVEL=2 - OMP_NUM_THREADS=1 - + # Skip testing during wheel build to avoid cross-compilation issues CIBW_TEST_SKIP: "*" diff --git a/pyproject.toml b/pyproject.toml index 9634a39..83487d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -177,11 +177,6 @@ before-all = [ "choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' || echo 'cmake already installed'", ] -# Test settings for all platforms -[tool.cibuildwheel.environment] -# Set OpenMP variables for better performance -OMP_NUM_THREADS = "1" - [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] From 1c9b47f8d060d2de9c3086550c62bed741ff76d9 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 06:43:58 +0800 Subject: [PATCH 16/29] Remove deprecated 'precise' mode alias; align mode docs The C library, CLI, and Python all expose four NSGA-III-derived presets: fast, default, recall, accurate. The 'precise' name was a deprecated alias for 'accurate' in the Python layer only; drop it now that 'extra' is heading to main. - python-kalign/__init__.py: remove MODE_PRECISE constant, the precise->accurate aliasing branch with DeprecationWarning, and the __all__ entry. Update ValueError text to list all four modes. - lib/include/kalign/kalign.h: add 'recall' to the kalign_get_mode_preset docstring (was listing only fast/default/accurate). - README-python.md: rewrite Modes table to show all four modes with the correct CLI syntax (--mode , not --fast/--precise which never existed); update Quick-Start and Ensemble examples accordingly. - tests: replace test_precise_mode cases with recall/accurate equivalents; drop MODE_PRECISE from constants assertion and expected exports. --- README-python.md | 43 ++++++++++++---------- lib/include/kalign/kalign.h | 2 +- python-kalign/__init__.py | 13 +------ tests/python/test_ecosystem_integration.py | 1 - tests/python/test_modes.py | 21 +++++------ 5 files changed, 37 insertions(+), 43 deletions(-) diff --git a/README-python.md b/README-python.md index 1f09c8c..78cf516 100644 --- a/README-python.md +++ b/README-python.md @@ -29,25 +29,28 @@ sequences = [ "ATCGATCATCG" ] -# Default mode — consistency anchors + VSM (best general-purpose) +# Default mode — single run with consistency anchors (best general-purpose) aligned = kalign.align(sequences) -# Fast mode — no consistency, fastest +# Fast mode — single run, fastest aligned = kalign.align(sequences, mode="fast") -# Precise mode — ensemble + realign, highest precision -aligned = kalign.align(sequences, mode="precise") +# Accurate mode — ensemble, highest precision +aligned = kalign.align(sequences, mode="accurate") ``` ## Modes -Kalign v3.5 provides three named modes that package the best configurations: +Kalign v3.5 provides four named modes, exposed identically through the Python API +and the `kalign` command-line tool. Presets were derived from NSGA-III +multi-objective optimization on BAliBASE v4 (protein) and BRAliBASE (RNA). | Mode | Python | CLI | Description | |------|--------|-----|-------------| -| **default** | `mode="default"` or omit | `kalign` | Consistency anchors + VSM. Best general-purpose. | -| **fast** | `mode="fast"` | `kalign --fast` | VSM only. Fastest, similar to kalign v3.4. | -| **precise** | `mode="precise"` | `kalign --precise` | Ensemble(3) + VSM + realign. Highest precision. | +| **fast** | `mode="fast"` | `kalign --mode fast` | Single run, fastest. | +| **default** | `mode="default"` or omit | `kalign` (or `--mode default`) | Single run with consistency anchors. Best general-purpose. | +| **recall** | `mode="recall"` | `kalign --mode recall` | Ensemble, optimized for recall. | +| **accurate** | `mode="accurate"` | `kalign --mode accurate` | Ensemble, highest precision. | Explicit parameters always override mode defaults: @@ -55,11 +58,12 @@ Explicit parameters always override mode defaults: # Fast base + 5 ensemble runs aligned = kalign.align(sequences, mode="fast", ensemble=5) -# Precise base + custom gap penalty -aligned = kalign.align(sequences, mode="precise", gap_open=8.0) +# Accurate base + custom gap penalty +aligned = kalign.align(sequences, mode="accurate", gap_open=8.0) ``` -Mode constants are also available: `kalign.MODE_DEFAULT`, `kalign.MODE_FAST`, `kalign.MODE_PRECISE`. +Mode constants are also available: `kalign.MODE_FAST`, `kalign.MODE_DEFAULT`, +`kalign.MODE_RECALL`, `kalign.MODE_ACCURATE`. ## Core API @@ -68,7 +72,7 @@ Mode constants are also available: `kalign.MODE_DEFAULT`, `kalign.MODE_FAST`, `k ```python aligned = kalign.align( sequences, # list of str - mode=None, # "default", "fast", "precise", or None (= default) + mode=None, # "fast", "default", "recall", "accurate", or None (= default) seq_type="auto", # "auto", "dna", "rna", "protein", "divergent", "internal" gap_open=None, # positive float, or None for defaults gap_extend=None, # positive float, or None for defaults @@ -103,8 +107,8 @@ Additional parameters for advanced use: ```python result = kalign.align_from_file( "input.fasta", - mode="precise", # or "default", "fast" - ensemble=5, # override: 5 runs instead of mode default (3) + mode="accurate", # or "fast", "default", "recall" + ensemble=5, # override: 5 runs instead of mode default min_support=0, # consensus threshold (0 = auto) save_poar="consensus.poar", # save POAR table for re-thresholding # load_poar="consensus.poar", # OR load pre-computed POAR @@ -142,13 +146,13 @@ Supported formats: `fasta`, `clustal`, `stockholm`, `phylip` (non-FASTA formats ## Ensemble Alignment & Confidence Scores -Ensemble mode runs multiple alignments with varied parameters and combines results via POAR (Pairs of Aligned Residues) consensus. The simplest way to use it is `mode="precise"` (ensemble=3 + realign). For more control, set `ensemble` directly. +Ensemble mode runs multiple alignments with varied parameters and combines results via POAR (Pairs of Aligned Residues) consensus. The simplest way to use it is `mode="accurate"` (5-run ensemble) or `mode="recall"` (recall-tuned ensemble). For more control, set `ensemble` directly. ```python import kalign -# Precise mode: ensemble(3) + realign — highest precision -result = kalign.align_from_file("proteins.fasta", mode="precise") +# Accurate mode: 5-run ensemble — highest precision +result = kalign.align_from_file("proteins.fasta", mode="accurate") # Or: explicit 5 ensemble runs result = kalign.align_from_file("proteins.fasta", ensemble=5) @@ -298,8 +302,9 @@ print(type(aln)) # ```bash # Modes kalign-py -i sequences.fasta -o aligned.fasta # default mode -kalign-py --fast -i sequences.fasta -o aligned.fasta # fast mode -kalign-py --precise -i sequences.fasta -o aligned.fasta # precise mode +kalign-py --mode fast -i sequences.fasta -o aligned.fasta # fast mode +kalign-py --mode recall -i sequences.fasta -o aligned.fasta # recall mode +kalign-py --mode accurate -i sequences.fasta -o aligned.fasta # accurate mode # I/O options kalign-py -i sequences.fasta -o - --format clustal # stdout diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 7ec1074..32021a5 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -127,7 +127,7 @@ EXTERN int kalign_generate_ensemble_runs(const struct kalign_run_config* base, * (objectives: F1, TC, wall_time) with 5-fold cross-validation on * BAliBASE v4 (protein) and BRAliBASE (RNA). * - * mode: "fast", "default", or "accurate" (case-insensitive). + * mode: "fast", "default", "recall", or "accurate" (case-insensitive). * NULL is treated as "default". * biotype: ALN_BIOTYPE_PROTEIN, ALN_BIOTYPE_DNA, or ALN_BIOTYPE_RNA. * Determines which preset grid slot to use. diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index 566768b..d2b5225 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -82,7 +82,6 @@ def __repr__(self): MODE_DEFAULT = "default" MODE_RECALL = "recall" MODE_ACCURATE = "accurate" -MODE_PRECISE = "precise" # deprecated alias for "accurate" # Valid preset modes (resolved by C library) _PRESET_MODES = {"fast", "default", "recall", "accurate"} @@ -118,20 +117,13 @@ def _resolve_seq_type(seq_type): def _resolve_mode_name(mode): - """Normalize mode name, handling 'precise' -> 'accurate' alias.""" + """Normalize mode name to one of the C-library presets.""" if mode is None: return "default" lower = mode.lower() - if lower == "precise": - warnings.warn( - 'mode="precise" is deprecated, use mode="accurate" instead.', - DeprecationWarning, - stacklevel=3, - ) - return "accurate" if lower not in _PRESET_MODES: raise ValueError( - f"Invalid mode: {mode!r}. Must be one of: 'default', 'fast', 'accurate'" + f"Invalid mode: {mode!r}. Must be one of: 'fast', 'default', 'recall', 'accurate'" ) return lower @@ -871,7 +863,6 @@ def write_confidence(path: str, result: AlignedSequences) -> None: "MODE_DEFAULT", "MODE_RECALL", "MODE_ACCURATE", - "MODE_PRECISE", "__version__", "__author__", "__email__", diff --git a/tests/python/test_ecosystem_integration.py b/tests/python/test_ecosystem_integration.py index 179a1bb..b0c5b1a 100644 --- a/tests/python/test_ecosystem_integration.py +++ b/tests/python/test_ecosystem_integration.py @@ -319,7 +319,6 @@ def test_module_exports(self): "MODE_DEFAULT", "MODE_RECALL", "MODE_ACCURATE", - "MODE_PRECISE", "PROTEIN_CORBLOSUM66", "__version__", "__author__", diff --git a/tests/python/test_modes.py b/tests/python/test_modes.py index c0aef33..b549ab4 100644 --- a/tests/python/test_modes.py +++ b/tests/python/test_modes.py @@ -1,4 +1,4 @@ -"""Tests for the unified mode interface (default/fast/precise).""" +"""Tests for the unified mode interface (fast/default/recall/accurate).""" import os import tempfile @@ -22,10 +22,10 @@ class TestModeConstants: """Test mode constant exports.""" def test_mode_constants_exist(self): - assert kalign.MODE_DEFAULT == "default" assert kalign.MODE_FAST == "fast" + assert kalign.MODE_DEFAULT == "default" + assert kalign.MODE_RECALL == "recall" assert kalign.MODE_ACCURATE == "accurate" - assert kalign.MODE_PRECISE == "precise" # deprecated alias class TestAlignModes: @@ -51,10 +51,9 @@ def test_fast_mode(self): assert len(result) == len(TEST_SEQUENCES) assert all(len(s) == len(result[0]) for s in result) - def test_precise_mode(self): - """mode='precise' is deprecated alias for 'accurate'.""" - with pytest.warns(DeprecationWarning, match="precise"): - result = kalign.align(TEST_SEQUENCES, mode="precise") + def test_recall_mode(self): + """mode='recall' produces alignment.""" + result = kalign.align(TEST_SEQUENCES, mode="recall") if isinstance(result, tuple): seqs = result[0] else: @@ -98,8 +97,8 @@ def test_fast_mode(self): names, sequences = result assert len(names) > 0 - def test_precise_mode(self): - result = kalign.align_from_file(TEST_FILE, mode="precise") + def test_accurate_mode(self): + result = kalign.align_from_file(TEST_FILE, mode="accurate") names, sequences = result assert len(names) > 0 @@ -126,11 +125,11 @@ def test_fast_mode(self): finally: os.unlink(out) - def test_precise_mode(self): + def test_accurate_mode(self): with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as f: out = f.name try: - kalign.align_file_to_file(TEST_FILE, out, mode="precise") + kalign.align_file_to_file(TEST_FILE, out, mode="accurate") assert os.path.getsize(out) > 0 finally: os.unlink(out) From dd01498993f769ecab2fa0203c51d0424ae68724 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 06:59:32 +0800 Subject: [PATCH 17/29] Remove paper-side benchmarks, optimizers, and PRDs Prepares the repo for public release by dropping material that belongs to the manuscript pipeline rather than the kalign library itself. Optimizers (moved to ~/Work/Documents/Manuscripts/2026_kalign_35/ scripts/optimizers before deletion; preserved there for reproducibility of the NSGA-III preset numbers shipped in 3.5): - benchmarks/optimize_params.py - benchmarks/optimize_unified.py - benchmarks/optimize_ensemble.py - benchmarks/optimize_parallel.py - benchmarks/PRD_unified_optimizer.md - benchmarks/PRD_ensemble_optimizer.md Analysis / visualisation scripts (paper repo has its own equivalents): - benchmarks/analysis.py - benchmarks/app.py - benchmarks/view_pareto.py - benchmarks/mumsa_plots.py, mumsa_precision.py - benchmarks/combined_improvements.py - benchmarks/full_comparison.py - benchmarks/external_balibase.py - benchmarks/make_summary_figure.py - benchmarks/eval_checkpoint_configs.py - benchmarks/vsm_ensemble_experiment.py - benchmarks/bench_quality_timing.py - benchmarks/run_balibase_comparison.py Other paper-side material: - benchmarks/PRD_kalign_align_full.md (superseded by docs/PRD-parameter-cleanup.md) - docs/PRD-benchmark-repo-update.md (hard-coded paper-repo paths) - Containerfile.downstream (explicitly labelled paper container) Kept: - benchmarks/runner.py, datasets.py, scoring.py (invoked by CI workflow benchmark.yml) - benchmarks/downstream/* (covered by tests/python/test_downstream_integration.py) - Containerfile, Containerfile.memcheck - docs/PRD-{msa-consistency,confidence-masking-and-add-sequences}.md - PRD_sparse_consistency.md Verified: benchmark package still imports; pytest tests/python/ passes (170 passed, 1 pre-existing test_module_exports failure unrelated to this cleanup). --- Containerfile.downstream | 153 --- benchmarks/PRD_ensemble_optimizer.md | 257 ---- benchmarks/PRD_kalign_align_full.md | 729 ----------- benchmarks/PRD_unified_optimizer.md | 421 ------ benchmarks/analysis.py | 574 -------- benchmarks/app.py | 603 --------- benchmarks/bench_quality_timing.py | 142 -- benchmarks/combined_improvements.py | 301 ----- benchmarks/eval_checkpoint_configs.py | 113 -- benchmarks/external_balibase.py | 162 --- benchmarks/full_comparison.py | 214 --- benchmarks/make_summary_figure.py | 248 ---- benchmarks/mumsa_plots.py | 220 ---- benchmarks/mumsa_precision.py | 291 ----- benchmarks/optimize_ensemble.py | 1102 ---------------- benchmarks/optimize_parallel.py | 444 ------- benchmarks/optimize_params.py | 1181 ----------------- benchmarks/optimize_unified.py | 1746 ------------------------- benchmarks/run_balibase_comparison.py | 77 -- benchmarks/view_pareto.py | 1014 -------------- benchmarks/vsm_ensemble_experiment.py | 307 ----- docs/PRD-benchmark-repo-update.md | 304 ----- 22 files changed, 10603 deletions(-) delete mode 100644 Containerfile.downstream delete mode 100644 benchmarks/PRD_ensemble_optimizer.md delete mode 100644 benchmarks/PRD_kalign_align_full.md delete mode 100644 benchmarks/PRD_unified_optimizer.md delete mode 100644 benchmarks/analysis.py delete mode 100644 benchmarks/app.py delete mode 100644 benchmarks/bench_quality_timing.py delete mode 100644 benchmarks/combined_improvements.py delete mode 100644 benchmarks/eval_checkpoint_configs.py delete mode 100644 benchmarks/external_balibase.py delete mode 100644 benchmarks/full_comparison.py delete mode 100644 benchmarks/make_summary_figure.py delete mode 100644 benchmarks/mumsa_plots.py delete mode 100644 benchmarks/mumsa_precision.py delete mode 100644 benchmarks/optimize_ensemble.py delete mode 100644 benchmarks/optimize_parallel.py delete mode 100644 benchmarks/optimize_params.py delete mode 100644 benchmarks/optimize_unified.py delete mode 100644 benchmarks/run_balibase_comparison.py delete mode 100644 benchmarks/view_pareto.py delete mode 100644 benchmarks/vsm_ensemble_experiment.py delete mode 100644 docs/PRD-benchmark-repo-update.md diff --git a/Containerfile.downstream b/Containerfile.downstream deleted file mode 100644 index 5081a31..0000000 --- a/Containerfile.downstream +++ /dev/null @@ -1,153 +0,0 @@ -# Kalign Paper Benchmark Container -# -# Single container with ALL tools at pinned versions for reproducible -# paper benchmarks. Includes alignment tools (kalign, ClustalO, MAFFT, -# MUSCLE), downstream tools (INDELible, HyPhy, IQ-TREE, HMMER, GUIDANCE2), -# and the full Python environment for running the manuscript pipeline. -# -# Build: -# podman build -f Containerfile.downstream -t kalign-paper . -# -# Run the manuscript pipeline: -# podman run --rm -it \ -# -v /path/to/2026_kalign_35:/manuscript \ -# -w /manuscript \ -# kalign-paper \ -# bash -c "uv pip install -e . && snakemake --cores 16" -# -# Run a single benchmark: -# podman run --rm -it \ -# -v ./benchmarks/data:/kalign/benchmarks/data \ -# -v ./benchmarks/results:/kalign/benchmarks/results \ -# kalign-paper \ -# python -m benchmarks.bench_quality_timing --threads 16 - -FROM ubuntu:24.04 - -ENV DEBIAN_FRONTEND=noninteractive - -# ── System dependencies ────────────────────────────────────────────── -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential cmake g++ git curl wget ca-certificates \ - python3 python3-pip python3-venv python3-dev \ - pkg-config zlib1g-dev libcurl4-openssl-dev libssl-dev libeigen3-dev libboost-dev \ - hmmer \ - perl libwww-perl libbio-perl-perl cpanminus \ - && rm -rf /var/lib/apt/lists/* - -RUN cpanm --notest Bio::Perl - -# ── MAFFT (latest from source) ─────────────────────────────────────── -RUN cd /tmp && \ - wget -q https://mafft.cbrc.jp/alignment/software/mafft-7.526-without-extensions-src.tgz && \ - tar xzf mafft-7.526-without-extensions-src.tgz && \ - cd mafft-7.526-without-extensions/core && \ - make clean && make -j"$(nproc)" && make install && \ - rm -rf /tmp/mafft-* - -# ── ClustalO (latest from source) ──────────────────────────────────── -RUN apt-get update && apt-get install -y --no-install-recommends \ - libargtable2-dev && rm -rf /var/lib/apt/lists/* && \ - cd /tmp && \ - wget -q http://www.clustal.org/omega/clustal-omega-1.2.4.tar.gz && \ - tar xzf clustal-omega-1.2.4.tar.gz && \ - cd clustal-omega-1.2.4 && \ - ./configure && make -j"$(nproc)" && make install && \ - rm -rf /tmp/clustal-omega-* - -# ── MUSCLE v5.3 (prebuilt binary) ──────────────────────────────────── -RUN wget -q https://github.com/rcedgar/muscle/releases/download/v5.3/muscle-linux-x86.v5.3 \ - -O /usr/local/bin/muscle && \ - chmod +x /usr/local/bin/muscle - -# ── INDELible v1.03 from source ────────────────────────────────────── -RUN cd /tmp && \ - git clone --depth 1 https://github.com/matsengrp/indelible.git && \ - cd indelible/src && \ - make && \ - cp indelible /usr/local/bin/ && \ - rm -rf /tmp/indelible - -# ── HyPhy from source ──────────────────────────────────────────────── -RUN cd /tmp && \ - git clone --depth 1 https://github.com/veg/hyphy.git && \ - cd hyphy && \ - cmake -DCMAKE_BUILD_TYPE=Release -DNOAVX=ON . && \ - make -j"$(nproc)" hyphy && \ - cp hyphy /usr/local/bin/hyphy && \ - cp -r res /usr/local/lib/hyphy && \ - rm -rf /tmp/hyphy -ENV HYPHY_LIB=/usr/local/lib/hyphy -ENV HYPHY_PATH=/usr/local/lib/hyphy - -# ── IQ-TREE 2 from source ──────────────────────────────────────────── -RUN cd /tmp && \ - git clone --depth 1 --recurse-submodules https://github.com/iqtree/iqtree2.git && \ - cd iqtree2 && \ - mkdir build && cd build && \ - cmake -DCMAKE_BUILD_TYPE=Release .. && \ - make -j"$(nproc)" && \ - cp iqtree2 /usr/local/bin/ && \ - rm -rf /tmp/iqtree2 - -# ── GUIDANCE2 from GitHub ──────────────────────────────────────────── -RUN cd /tmp && \ - git clone --depth 1 https://github.com/anzaika/guidance.git && \ - cd guidance && make && \ - mkdir -p /opt/guidance-root && \ - cp -r www/Guidance /opt/guidance-root/Guidance && \ - cp -r www/Selecton /opt/guidance-root/Selecton && \ - cp -r www/bioSequence_scripts_and_constants /opt/guidance-root/bioSequence_scripts_and_constants && \ - mkdir -p /opt/guidance-root/Guidance/exec && \ - cp programs/msa_set_score/msa_set_score /opt/guidance-root/Guidance/exec/ && \ - cp programs/removeTaxa/removeTaxa /opt/guidance-root/Guidance/exec/ && \ - cp programs/isEqualTree/isEqualTree /opt/guidance-root/Guidance/exec/ && \ - chmod +x /opt/guidance-root/Guidance/guidance.pl && \ - mkdir -p /opt/programs/semphy /opt/programs/msa_set_score \ - /opt/programs/removeTaxa /opt/programs/isEqualTree && \ - cp programs/semphy/semphy /opt/programs/semphy/ && \ - cp programs/msa_set_score/msa_set_score /opt/programs/msa_set_score/ && \ - cp programs/removeTaxa/removeTaxa /opt/programs/removeTaxa/ && \ - cp programs/isEqualTree/isEqualTree /opt/programs/isEqualTree/ && \ - printf '#!/bin/sh\nexec perl /opt/guidance-root/Guidance/guidance.pl "$@"\n' \ - > /usr/local/bin/guidance2 && \ - chmod +x /usr/local/bin/guidance2 && \ - rm -rf /tmp/guidance - -# ── Kalign (threadpool build) ───────────────────────────────────────── -COPY . /kalign -WORKDIR /kalign - -RUN mkdir cbuild && cd cbuild && \ - cmake -DCMAKE_BUILD_TYPE=Release -DUSE_OPENMP=OFF -DUSE_THREADPOOL=ON .. && \ - make -j"$(nproc)" - -# ── Record tool versions at build time ──────────────────────────────── -RUN echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /tool_versions.txt && \ - echo "kalign=$(cbuild/src/kalign --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ - echo "mafft=$(mafft --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ - echo "muscle=$(muscle -version 2>&1 || echo unknown)" >> /tool_versions.txt && \ - echo "clustalo=$(clustalo --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ - echo "hmmer=$(hmmbuild -h 2>&1 | grep '^# HMMER' | head -1 || echo unknown)" >> /tool_versions.txt && \ - echo "iqtree=$(iqtree2 --version 2>&1 | grep 'IQ-TREE' | head -1 || echo unknown)" >> /tool_versions.txt && \ - echo "indelible=1.03" >> /tool_versions.txt && \ - echo "hyphy=$(hyphy --version 2>&1 | head -1 || echo unknown)" >> /tool_versions.txt && \ - cat /tool_versions.txt - -# ── Python environment ──────────────────────────────────────────────── -RUN python3 -m venv /venv -ENV PATH="/venv/bin:/kalign/cbuild/src:$PATH" - -RUN pip install --no-cache-dir uv && \ - uv pip install --no-cache -e ".[benchmark]" \ - --config-settings='cmake.args=-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON' && \ - uv pip install --no-cache \ - dendropy biopython pandas matplotlib scipy seaborn numpy snakemake - -# ── Verify tools ────────────────────────────────────────────────────── -RUN which kalign && which clustalo && which mafft && which muscle && \ - which hmmbuild && which hmmsearch && which iqtree2 && \ - which hyphy && which indelible && which guidance2 - -# ── Data directories ────────────────────────────────────────────────── -RUN mkdir -p benchmarks/data/downloads benchmarks/results diff --git a/benchmarks/PRD_ensemble_optimizer.md b/benchmarks/PRD_ensemble_optimizer.md deleted file mode 100644 index 99edd7b..0000000 --- a/benchmarks/PRD_ensemble_optimizer.md +++ /dev/null @@ -1,257 +0,0 @@ -# PRD: Ensemble Parameter Optimizer - -## Goal - -Build `benchmarks/optimize_ensemble.py` — an NSGA-II optimizer that finds the best per-run parameters for kalign's ensemble alignment mode. This is the companion to `optimize_params.py` (single-run optimizer) but targets the ensemble pipeline where multiple diverse alignments are combined via POAR consensus. - -## Background - -The ensemble pipeline runs N independent alignments with different settings, then combines them using a Partial Order Alignment Representation (POAR) consensus. Currently, the per-run diversity is controlled by a hardcoded scale-factor table in `ensemble.c`. We've added `kalign_ensemble_custom()` which accepts fully independent per-run parameters, enabling proper optimization. - -Key insight from prior experiments: the consistency transform (anchor-based alignment improvement) has never been tested inside ensemble runs. It should boost the quality of each individual input alignment without reducing diversity, since diversity comes from gap penalties/matrices/noise, not from anchoring. - -## Search Space - -### Per-run parameters (× N runs) - -Each of the N runs gets its own: - -| Parameter | Range | Type | Description | -|-----------|-------|------|-------------| -| `gpo` | [2.0, 15.0] | continuous | Gap open penalty | -| `gpe` | [0.5, 5.0] | continuous | Gap extend penalty | -| `tgpe` | [0.1, 3.0] | continuous | Terminal gap extend penalty | -| `matrix` | {PFASUM43, PFASUM60, CorBLOSUM66} | discrete | Substitution matrix | -| `noise` | [0.0, 0.5] | continuous | Tree perturbation noise sigma | - -= 5 parameters per run - -### Shared parameters (apply to all runs) - -| Parameter | Range | Type | Description | -|-----------|-------|------|-------------| -| `vsm_amax` | [0.0, 5.0] | continuous | Variable scoring matrix strength | -| `consistency` | {0, 1, 2, 3, 5, 8} | discrete | Anchor consistency rounds per run | -| `consistency_weight` | [0.5, 5.0] | continuous | Consistency bonus weight | -| `realign` | {0, 1, 2} | discrete | Tree-rebuild iterations per run | -| `min_support` | {0, 1, 2, ..., N} | discrete | POAR consensus threshold (0 = auto) | - -= 5 shared parameters - -### Total parameter counts by N runs - -| N runs | Per-run | Shared | Total | Recommended pop_size | -|--------|---------|--------|-------|---------------------| -| 3 | 15 | 5 | 20 | 100 | -| 5 | 25 | 5 | 30 | 150 | -| 8 | 40 | 5 | 45 | 200 | - -## Architecture - -### Script: `benchmarks/optimize_ensemble.py` - -Follows the same architecture as `optimize_params.py`: - -``` -optimize_ensemble.py -├── Parameter space definition (encode/decode per-run arrays) -├── evaluate_ensemble_params() — run one ensemble config on a case list -├── evaluate_ensemble_cv() — stratified k-fold CV wrapper -├── _eval_one_ensemble() — top-level function for ProcessPoolExecutor -├── Dashboard (rich live) — same look & feel as optimize_params.py -├── EnsembleCVProblem (pymoo) — NSGA-II problem with serial/parallel eval -├── GenerationCallback — dashboard updates + checkpoint saving -├── load_checkpoint() — resume from pickle -└── main() — CLI entry point -``` - -### Key differences from `optimize_params.py` - -1. **Parameter encoding**: The decision vector has a variable-length per-run section. For N=3: `[gpo_0, gpe_0, tgpe_0, noise_0, ..., gpo_2, gpe_2, tgpe_2, noise_2, vsm_amax, consistency_weight, matrix_0, matrix_1, matrix_2, consistency, realign, min_support]`. The `decode_params()` function returns a dict with lists for per-run params. - -2. **Alignment call**: Uses `kalign._core.ensemble_custom_file_to_file()` instead of `kalign.align_file_to_file()`. - -3. **Fixed N runs**: The `--n-runs` CLI argument sets N (default: 3). Separate optimization runs for different N values, then compare Pareto fronts. - -4. **Display**: The dashboard `format_params_short()` shows per-run params compactly, e.g. `R0: gpo=7.0/PFASUM43 R1: gpo=3.5/PFASUM60 R2: gpo=10.5/CorBLOSUM66 | vsm=2.0 c=3 re=1 ms=2`. - -### CLI interface - -```bash -# Quick test -uv run python -m benchmarks.optimize_ensemble --n-runs 3 --pop-size 20 --n-gen 5 - -# Production run on Threadripper (3 runs) -uv run python -m benchmarks.optimize_ensemble \ - --n-runs 3 --pop-size 100 --n-gen 50 \ - --n-workers 56 --n-threads 1 --n-folds 5 - -# Production run (5 runs) -uv run python -m benchmarks.optimize_ensemble \ - --n-runs 5 --pop-size 150 --n-gen 60 \ - --n-workers 56 --n-threads 1 - -# Production run (8 runs) -uv run python -m benchmarks.optimize_ensemble \ - --n-runs 8 --pop-size 200 --n-gen 80 \ - --n-workers 56 --n-threads 1 - -# Resume after interrupt -uv run python -m benchmarks.optimize_ensemble \ - --resume benchmarks/results/ensemble_optim/gen_checkpoint.pkl \ - --n-gen 80 --n-workers 56 - -# Add wall time as 3rd objective -uv run python -m benchmarks.optimize_ensemble --n-runs 3 --optimize-time -``` - -### Arguments - -| Argument | Default | Description | -|----------|---------|-------------| -| `--n-runs` | 3 | Number of ensemble runs (fixed per optimization) | -| `--pop-size` | 100 | NSGA-II population size | -| `--n-gen` | 50 | Total number of generations | -| `--n-folds` | 5 | Stratified CV folds | -| `--n-threads` | 1 | OpenMP threads per alignment | -| `--n-workers` | 1 | Parallel worker processes | -| `--seed` | 42 | Random seed | -| `--optimize-time` | false | Add wall time as 3rd objective | -| `--output-dir` | `benchmarks/results/ensemble_optim` | Output directory | -| `--no-dashboard` | false | Disable rich dashboard | -| `--resume` | None | Path to checkpoint file for resume | - -## Objectives - -Same as single-run optimizer: -1. **Maximize F1** (category-averaged, held-out CV folds) -2. **Maximize TC** (category-averaged, held-out CV folds) -3. *(Optional)* **Minimize wall time** - -## Evaluation pipeline - -For each individual in the population: - -``` -decode_params(x) → per-run arrays + shared params - ↓ -for each CV fold: - for each test case: - kalign._core.ensemble_custom_file_to_file( - input, output, - run_gpo=[gpo_0, ..., gpo_N], - run_gpe=[gpe_0, ..., gpe_N], - run_tgpe=[tgpe_0, ..., tgpe_N], - run_noise=[noise_0, ..., noise_N], - run_types=[matrix_0, ..., matrix_N], - vsm_amax=vsm_amax, - realign=realign, - consistency_anchors=consistency, - consistency_weight=consistency_weight, - min_support=min_support, - refine=REFINE_CONFIDENT, # always on for ensemble - seed=42, - ) - score_alignment_detailed(reference, output) - ↓ - category-averaged F1, TC for this fold - ↓ -mean across folds → CV F1, CV TC -``` - -## Baseline - -The baseline for comparison is the current best ensemble result: -- `ens3+vsm+ref+ra1`: F1=0.768, TC=0.467 -- Uses hardcoded scale-factor table, no consistency, PFASUM43 for all runs - -This will be computed via `kalign.align_file_to_file(ensemble=3, vsm_amax=2.0, refine="confident", realign=1)` at startup. - -## Dashboard - -Same rich live dashboard as `optimize_params.py`: - -``` -┌─ Progress ──────────────────────────────────────────────────┐ -│ Gen 12/50 Eval 480/5000 Elapsed 45.2m ETA 142m │ -│ Current: R0:gpo=7.0 R1:gpo=3.5 R2:gpo=10.5 | vsm=2.0 c=3 │ -├─ Best Solutions ────────────────────────────────────────────┤ -│ Baseline: F1=0.7680 TC=0.4670 │ -│ │ -│ Best F1: 0.7823 (+0.0143) │ -│ R0:7.0/P43 R1:3.5/P60 R2:10.5/CB66 | vsm=1.8 c=3 re=1 │ -│ │ -│ Best TC: 0.4891 (+0.0221) │ -│ R0:6.5/P43 R1:4.0/P43 R2:9.0/P60 | vsm=2.1 c=5 re=1 │ -├─ Pareto Front ──────────────────────────────────────────────┤ -│ # │ CV F1 │ CV TC │ Parameters │ -│ 0 │ 0.7823 │ 0.4801 │ R0:7.0/P43 R1:3.5/P60 ... │ -│ 1 │ 0.7791 │ 0.4891 │ R0:6.5/P43 R1:4.0/P43 ... │ -│ ... │ -├─ Trend ─────────────────────────────────────────────────────┤ -│ Gen 1: F1=0.7512 Gen 5: F1=0.7634 Gen 10: F1=0.7789 │ -├─ Recent ────────────────────────────────────────────────────┤ -│ F1=0.7654 TC=0.4512 R0:8.1/P43 R1:5.2/CB66 R2:3.0/P60 │ -│ F1=0.7823 TC=0.4801 R0:7.0/P43 R1:3.5/P60 R2:10.5/CB66 │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Checkpoint / Resume - -Identical to `optimize_params.py`: -- `GenerationCallback` saves `gen_checkpoint.pkl` after every completed generation -- Stores: population X and F arrays, generation count, evaluation history -- Atomic write (write to .tmp, rename) -- `--resume` loads checkpoint, reconstructs NSGA2 with saved population as initial sampling -- Remaining generations = `--n-gen` minus completed generations -- `--n-workers` and `--n-threads` can change between runs -- `--n-folds`, `--seed`, `--n-runs` must stay the same - -## Clean interrupt - -Identical to `optimize_params.py`: -- `_kill_pool()` sends SIGTERM to worker processes on KeyboardInterrupt -- `os._exit(1)` in main handler to skip atexit join-hangs -- Single Ctrl+C exits cleanly, prints checkpoint path and resume command - -## Output files - -Written to `--output-dir` (default: `benchmarks/results/ensemble_optim`): - -1. **`gen_checkpoint.pkl`** — per-generation checkpoint for resume -2. **`pareto_front.txt`** — human-readable Pareto front with full per-run parameters -3. **`optim_checkpoint.pkl`** — final results pickle (Pareto configs, history, baselines) - -## Post-optimization analysis - -After optimization completes: -1. Print Pareto front as rich table -2. Re-evaluate best-F1 solution on full dataset (all 218 cases) -3. Show per-category breakdown (RV11–RV50) comparing optimized vs baseline -4. Overfit check: flag if full-dataset score exceeds CV score by >0.02 - -## Code reuse - -The following can be imported from `optimize_params.py` or a shared module: -- `stratified_kfold()` — fold splitting logic -- `score_alignment_detailed()` — already in `scoring.py` -- `BenchmarkCase`, `balibase_cases`, etc. — already in `datasets.py` - -The following must be new (ensemble-specific): -- `decode_ensemble_params()` / `encode_ensemble_params()` -- `evaluate_ensemble_params()` — calls `ensemble_custom_file_to_file` -- `evaluate_ensemble_cv()` — CV wrapper -- `format_ensemble_params_short()` / `format_ensemble_params_long()` -- `EnsembleCVProblem` — pymoo Problem subclass -- Dashboard and callback can be adapted from the existing ones - -## Implementation order - -1. Parameter space definition + encode/decode -2. `evaluate_ensemble_params()` + `evaluate_ensemble_cv()` -3. `EnsembleCVProblem` with serial and parallel evaluation -4. Dashboard (adapt from existing) -5. `GenerationCallback` with checkpoint saving -6. `main()` with CLI, baseline, optimization loop, results -7. Resume logic -8. Smoke test with `--pop-size 4 --n-gen 2 --n-runs 3` diff --git a/benchmarks/PRD_kalign_align_full.md b/benchmarks/PRD_kalign_align_full.md deleted file mode 100644 index 7b324cb..0000000 --- a/benchmarks/PRD_kalign_align_full.md +++ /dev/null @@ -1,729 +0,0 @@ -# PRD: Unified C Entry Point — `kalign_align_full` - -## Goal - -Replace the 7+ separate alignment entry points in kalign's C library with a single comprehensive function `kalign_align_full()` that accepts an array of per-run configs. All callers (CLI, Python bindings, benchmark optimizer) route through this one function, eliminating duplicated routing logic, silent parameter dropping, and inconsistent sentinel handling. - -## Current State: Detailed Audit - -### C Library Entry Points (`lib/src/aln_wrap.c` + `lib/src/ensemble.c`) - -#### 1. `kalign()` — aln_wrap.c line 110 - -```c -int kalign(char **seq, int *len, int numseq, int n_threads, int type, - float gpo, float gpe, float tgpe, char ***aligned, int *out_aln_len) -``` - -- **What it does**: Wraps `kalign_run()` with arr→msa→arr conversion. -- **Hardcoded**: `refine=NONE`, `adaptive_budget=0`, `quiet=1` -- **Missing**: Everything else (vsm, seq_weights, consistency, realign, ensemble, dist_scale) -- **Used by**: Legacy C API consumers only - -#### 2. `kalign_run()` — aln_wrap.c line 263 - -```c -int kalign_run(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, int refine, int adaptive_budget) -``` - -- **What it does**: Thin wrapper around `kalign_run_seeded()`. -- **Hardcoded**: `tree_seed=0, tree_noise=0.0, dist_scale=0.0, vsm_amax=-1.0, use_seq_weights=-1.0, consistency_anchors=0, consistency_weight=2.0` -- **Missing**: All advanced features -- **Used by**: `kalign()` wrapper above, and the pybind11 router as the "most basic" fallback - -#### 3. `kalign_run_seeded()` — aln_wrap.c line 133 - -```c -int kalign_run_seeded(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - uint64_t tree_seed, float tree_noise, - float dist_scale, float vsm_amax, - float use_seq_weights, - int consistency_anchors, float consistency_weight) -``` - -- **What it does**: Full single-run alignment with all knobs. -- **Sentinel handling**: - - `use_seq_weights >= 0.0` → override (default is 0.0 from `aln_param_init`) - - `dist_scale > 0.0` → override (GUARDED: 0.0 means "keep default", which is also 0.0) - - `vsm_amax >= 0.0` → override (default is 2.0 for protein, 0.0 for DNA) -- **Missing**: realign iterations (separate function), ensemble -- **Consistency**: YES — builds anchor table if `consistency_anchors > 0`, frees it after refinement -- **Used by**: pybind11 router (when consistency > 0), CLI fallback, `kalign_run()`, ensemble per-run alignments - -#### 4. `kalign_run_dist_scale()` — aln_wrap.c line 268 - -```c -int kalign_run_dist_scale(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - float use_seq_weights) -``` - -- **What it does**: Like `kalign_run_seeded` but WITHOUT tree noise and WITHOUT consistency. -- **Sentinel handling**: - - `dist_scale` → UNCONDITIONAL assignment (differs from seeded!) - - `vsm_amax >= 0.0` → override - - `use_seq_weights >= 0.0` → override -- **Missing**: `tree_seed`, `tree_noise`, `consistency_anchors`, `consistency_weight` -- **BUG/GOTCHA**: No consistency support. If pybind11 router picks this path, consistency params are silently dropped. -- **Used by**: pybind11 router (when `dist_scale > 0 || vsm_amax >= 0 || seq_weights >= 0` but no consistency and no realign) - -#### 5. `kalign_run_realign()` — aln_wrap.c line 361 - -```c -int kalign_run_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights, - int consistency_anchors, float consistency_weight) -``` - -- **What it does**: Full single-run + iterative tree rebuild from alignment distances. -- **Sentinel handling**: - - `dist_scale` → UNCONDITIONAL assignment - - `vsm_amax >= 0.0` → override - - `use_seq_weights >= 0.0` → override -- **Key behavior**: Builds initial tree from BPM, then after first alignment: finalise → compute pairwise distances → strip gaps → rebuild UPGMA tree → re-align. Consistency table built ONCE before first alignment (not rebuilt per iteration). -- **Missing**: `tree_seed`, `tree_noise` (always deterministic initial tree) -- **Used by**: pybind11 router (when `realign > 0`), CLI (when `realign > 0`), ensemble per-run alignments (when `realign > 0`) - -#### 6. `kalign_ensemble()` — ensemble.c line 223 - -```c -int kalign_ensemble(struct msa* msa, int n_threads, int type, - int n_runs, float gpo, float gpe, float tgpe, - uint64_t seed, int min_support, - const char* save_poar_path, - int refine, float dist_scale, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight) -``` - -- **What it does**: N runs with hardcoded scale-factor diversity table, POAR consensus/selection. -- **Hardcoded values**: - - `use_seq_weights`: forced to 0.0 when `-1.0` (sentinel). Comment says "hurts ensemble performance". - - Per-run diversity from `run_params[]` table: 12 entries with `gpo_scale`, `gpe_scale`, `tgpe_scale`, `noise` multipliers. - - Run 0 always uses base params, no noise. - - Post-selection refinement: HARDCODES `KALIGN_REFINE_CONFIDENT` for the refinement re-run (line 421), regardless of the `refine` param passed for per-run alignments. - - Post-selection refinement: HARDCODES `dist_scale=0.0f` (line 423). - - Auto min_support when `min_support=0`: `min_sup = (n_runs + 2) / 3`, clamped to >= 2. -- **Missing**: Per-run matrix types (always uses same `type` for all runs) -- **Used by**: pybind11 router (when `ensemble > 0`), CLI (when `ensemble > 0`) - -#### 7. `kalign_ensemble_custom()` — ensemble.c line 515 - -```c -int kalign_ensemble_custom(struct msa* msa, int n_threads, int type, - int n_runs, - const float* run_gpo, const float* run_gpe, - const float* run_tgpe, const int* run_types, - const float* run_noise, - uint64_t seed, int min_support, - int refine, float vsm_amax, - int realign, float use_seq_weights, - int consistency_anchors, float consistency_weight) -``` - -- **What it does**: Like `kalign_ensemble` but with per-run gap penalties, types, and noise. -- **Hardcoded values**: - - `use_seq_weights`: forced to 0.0 when `-1.0` (sentinel), same as `kalign_ensemble`. - - Post-selection refinement: HARDCODES `KALIGN_REFINE_CONFIDENT` (line 676). - - Post-selection refinement: HARDCODES `dist_scale=0.0f` (line 678). - - Post-selection refinement: HARDCODES `adaptive_budget=0` (line 675). - - Auto min_support: same `(n_runs + 2) / 3` formula. -- **Missing**: Per-run `dist_scale`, `vsm_amax`, `seq_weights`, `consistency` (all shared across runs). `save_poar` (removed from this function). -- **Used by**: pybind11 `ensemble_custom_file_to_file()` only (not accessible from CLI or Python wrapper) - -#### 8. `kalign_post_realign()` — aln_wrap.c line 539 - -```c -int kalign_post_realign(struct msa *msa, int n_threads, int type, - float gpo, float gpe, float tgpe, - int refine, int adaptive_budget, - float dist_scale, float vsm_amax, - int realign_iterations, - float use_seq_weights) -``` - -- **What it does**: Takes an already-finalized MSA (e.g., from ensemble) and does realign iterations. -- **Missing**: `consistency_anchors`, `consistency_weight` — no consistency support in post-realign! -- **Used by**: Was intended for post-ensemble realign, but this was found to HURT performance (0.758→0.708) so it's currently unused. - -### Routing Logic Comparison - -#### pybind11 `run_alignment()` (`_core.cpp:73-104`) - -``` -if load_poar → kalign_consensus_from_poar() -elif ensemble > 0 → kalign_ensemble() ← old hardcoded diversity, NOT ensemble_custom -elif realign > 0 → kalign_run_realign() ← OK, has consistency -elif consistency > 0 → kalign_run_seeded() ← OK -elif dist_scale > 0 → kalign_run_dist_scale() ← BUG: drops consistency! - OR vsm_amax >= 0 - OR seq_weights >= 0 -else → kalign_run() ← drops everything advanced -``` - -**Known bugs in the router**: -1. If you pass `vsm_amax=2.0` and `consistency=5`, the router picks `kalign_run_seeded` (correct, because `consistency > 0` is checked first). But if you pass `vsm_amax=2.0` and `consistency=0`, the router picks `kalign_run_dist_scale` which doesn't support consistency — OK in this case since consistency=0, but fragile logic. -2. `kalign_ensemble()` is the old hardcoded version. `ensemble_custom` is only accessible via a separate function. - -#### CLI `run_kalign()` (`src/run_kalign.c:409-464`) - -``` -if load_poar → kalign_consensus_from_poar() -elif ensemble > 0 → kalign_ensemble() ← old hardcoded diversity only -elif realign > 0 → kalign_run_realign() -else → kalign_run_seeded() ← always this for single-run -``` - -**Key differences from pybind11 router**: -1. CLI always uses `kalign_run_seeded` for non-ensemble/non-realign — never falls through to `kalign_run_dist_scale` or `kalign_run`. This means the CLI consistently handles vsm_amax and seq_weights correctly. -2. CLI has `--fast` (consistency=0) and `--precise` (ensemble=3, realign=1) modes. -3. CLI has no access to `ensemble_custom` at all. - -### Sentinel Value Summary - -| Parameter | Sentinel | Meaning | Where checked | -|-----------|----------|---------|---------------| -| `gpo` | -1.0 | Use matrix default | `aln_param_init`: `if(gpo >= 0.0)` | -| `gpe` | -1.0 | Use matrix default | `aln_param_init`: `if(gpe >= 0.0)` | -| `tgpe` | -1.0 | Use matrix default | `aln_param_init`: `if(tgpe >= 0.0)` | -| `vsm_amax` | -1.0 | Use biotype default (2.0 protein, 0.0 DNA) | `if(vsm_amax >= 0.0)` | -| `use_seq_weights` | -1.0 | Use biotype default (0.0 for all) | `if(use_seq_weights >= 0.0)` | -| `dist_scale` | 0.0 | Off | Inconsistent: guarded in seeded, unconditional in others | -| `tree_seed` | 0 | Deterministic tree | `if(tree_seed != 0 && tree_noise > 0.0f)` | -| `tree_noise` | 0.0 | No perturbation | `if(tree_seed != 0 && tree_noise > 0.0f)` | -| `consistency_anchors` | 0 | Off | `if(consistency_anchors > 0)` | -| `consistency_weight` | 2.0 | Default weight | Only used when consistency > 0 | -| `adaptive_budget` | 0 | Off | Direct flag check | -| `refine` | 0 (NONE) | No refinement | Switch/if chain | -| `realign` | 0 | No tree rebuild | `if(realign > 0)` triggers `kalign_run_realign` | - -### Inconsistencies Found - -1. **`dist_scale` sentinel handling**: In `kalign_run_seeded`, `dist_scale` is GUARDED (`if(dist_scale > 0.0f)`), so 0.0 means "keep default" (which is also 0.0 — benign). In `kalign_run_dist_scale`, `kalign_run_realign`, and `kalign_post_realign`, it's UNCONDITIONAL (`ap->dist_scale = dist_scale`). Effect: none in practice (default is 0.0), but inconsistent code. - -2. **`use_seq_weights` in ensemble**: Both `kalign_ensemble` and `kalign_ensemble_custom` force `use_seq_weights = 0.0` when the sentinel `-1.0` is passed (lines 249-250, 545-546). But if an explicit positive value is passed, it flows through. Net effect: seq_weights is always 0 in ensemble mode through all current code paths, but it's not technically blocked at the C level for explicit positive values. - -3. **Post-selection refinement mode**: In both ensemble functions, the post-selection refinement HARDCODES `KALIGN_REFINE_CONFIDENT` regardless of the `refine` parameter. The `refine` parameter is used for the per-run alignments, but the post-selection re-run always uses CONFIDENT. This is intentional but undocumented. - -4. **`kalign_run_dist_scale` drops consistency**: This function doesn't accept consistency parameters at all. The pybind11 router can fall into this path when `vsm_amax` or `seq_weights` is set but `consistency=0` and `realign=0`. Currently benign (consistency=0 means no consistency anyway), but fragile. - -5. **`ensemble_custom` missing per-run params**: `vsm_amax`, `use_seq_weights`, `consistency_anchors`, `consistency_weight`, `dist_scale` are all shared across runs in `kalign_ensemble_custom`. This is an arbitrary limitation — the optimizer would benefit from per-run control of all these. - -## Proposed Design - -### Core principle: every run is fully described by one config - -Instead of "shared params + per-run overrides", each run gets its own complete config. For ensemble, you pass an array. No ambiguity about what's shared vs per-run. - -### Per-run config struct - -```c -/* kalign_config.h */ - -/* Describes everything needed for a single alignment run. */ -struct kalign_run_config { - /* Sequence type / substitution matrix */ - int type; /* KALIGN_TYPE_* constant (UNDEFINED = auto-detect) */ - - /* Gap penalties (-1.0 = use matrix defaults) */ - float gpo; /* gap open penalty */ - float gpe; /* gap extend penalty */ - float tgpe; /* terminal gap extend penalty */ - - /* Scoring modifiers */ - float vsm_amax; /* variable scoring matrix amplitude (-1.0 = biotype default) */ - float dist_scale; /* distance-dependent gap scaling (0.0 = off) */ - float use_seq_weights; /* profile rebalancing pseudocount (-1.0 = biotype default) */ - - /* Consistency transform */ - int consistency_anchors; /* number of anchor sequences K (0 = off) */ - float consistency_weight; /* bonus scale for consistency (default: 2.0) */ - - /* Refinement */ - int refine; /* KALIGN_REFINE_* constant (default: NONE) */ - int adaptive_budget; /* scale refinement trials by uncertainty (0 = off) */ - - /* Realign (iterative tree rebuild) */ - int realign; /* number of realign iterations (0 = off) */ - - /* Guide tree perturbation */ - uint64_t tree_seed; /* random seed for noisy tree (0 = deterministic) */ - float tree_noise; /* tree perturbation sigma (0.0 = none) */ -}; -``` - -### Ensemble config struct - -```c -/* Controls orchestration when n_runs > 1. */ -struct kalign_ensemble_config { - uint64_t seed; /* base RNG seed for diversity generation */ - int min_support; /* POAR consensus threshold (0 = auto) */ - const char* save_poar; /* path to save POAR table (NULL = don't save) */ -}; -``` - -### Initialization functions - -```c -/* Returns a run config with all sentinel/default values. */ -struct kalign_run_config kalign_run_config_defaults(void); - -/* Returns an ensemble config with defaults. */ -struct kalign_ensemble_config kalign_ensemble_config_defaults(void); -``` - -Default values for `kalign_run_config_defaults()`: -```c -struct kalign_run_config kalign_run_config_defaults(void) { - struct kalign_run_config cfg; - cfg.type = KALIGN_TYPE_UNDEFINED; - cfg.gpo = -1.0f; /* sentinel: use matrix default */ - cfg.gpe = -1.0f; - cfg.tgpe = -1.0f; - cfg.vsm_amax = -1.0f; /* sentinel: use biotype default */ - cfg.dist_scale = 0.0f; /* off */ - cfg.use_seq_weights = -1.0f; /* sentinel: use biotype default */ - cfg.consistency_anchors = 0; /* off */ - cfg.consistency_weight = 2.0f; - cfg.refine = KALIGN_REFINE_NONE; - cfg.adaptive_budget = 0; - cfg.realign = 0; - cfg.tree_seed = 0; /* deterministic */ - cfg.tree_noise = 0.0f; /* no perturbation */ - return cfg; -} -``` - -Default values for `kalign_ensemble_config_defaults()`: -```c -struct kalign_ensemble_config kalign_ensemble_config_defaults(void) { - struct kalign_ensemble_config ens; - ens.seed = 42; - ens.min_support = 0; /* auto: (n_runs + 2) / 3 */ - ens.save_poar = NULL; - return ens; -} -``` - -### The unified function - -```c -int kalign_align_full( - struct msa* msa, - const struct kalign_run_config* runs, /* array of run configs */ - int n_runs, /* 1 = single-run, >1 = ensemble */ - const struct kalign_ensemble_config* ens, /* NULL when n_runs == 1 */ - int n_threads -); -``` - -### Diversity table helper - -The old `kalign_ensemble()` auto-generates per-run diversity (scaled gap penalties + tree noise) from a base config. This becomes a standalone helper that **generates** the run config array: - -```c -/* Expand one base config into n_runs configs using the built-in diversity table. - Copies base into each slot, then applies gap penalty scale factors and tree noise. - Run 0 always gets the base config unchanged. - Caller allocates out[n_runs]. */ -int kalign_generate_ensemble_runs( - const struct kalign_run_config* base, - int n_runs, - uint64_t seed, - struct kalign_run_config* out -); -``` - -### Internal logic (pseudocode) - -``` -kalign_align_full(msa, runs, n_runs, ens, n_threads): - essential_input_check(msa) - detect_alphabet(msa) - - if n_runs > 1: - // ENSEMBLE PATH - for k = 0..n_runs-1: - copy = deep_copy(msa) - resolve sentinels in runs[k] (vsm_amax, seq_weights, gpo/gpe/tgpe) - if runs[k].realign > 0: - kalign_run_realign(copy, runs[k] params...) - else: - kalign_run_seeded(copy, runs[k] params...) - extract POAR from alignment - keep alignment for scoring - - score all alignments against POAR - decide: consensus vs selection (based on ens->min_support) - if selection: post-selection refinement (REFINE_CONFIDENT) - copy winner back to msa - compute confidence from POAR - - else: - // SINGLE-RUN PATH - resolve sentinels in runs[0] - if runs[0].realign > 0: - kalign_run_realign(msa, runs[0] params...) - else: - kalign_run_seeded(msa, runs[0] params...) - - sort by original rank - return OK -``` - -## Usage Examples - -### Single run with defaults - -```c -struct kalign_run_config run = kalign_run_config_defaults(); -kalign_align_full(msa, &run, 1, NULL, 4); -``` - -### Single run with custom params - -```c -struct kalign_run_config run = kalign_run_config_defaults(); -run.gpo = 8.0f; -run.vsm_amax = 2.0f; -run.consistency_anchors = 5; -run.realign = 1; -kalign_align_full(msa, &run, 1, NULL, 4); -``` - -### Ensemble with auto-diversity (replaces `kalign_ensemble`) - -```c -struct kalign_run_config base = kalign_run_config_defaults(); -base.vsm_amax = 2.0f; -base.realign = 1; - -struct kalign_run_config runs[3]; -kalign_generate_ensemble_runs(&base, 3, 42, runs); - -struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); -kalign_align_full(msa, runs, 3, &ens, 4); -``` - -### Ensemble with fully custom per-run params (optimizer use case) - -```c -struct kalign_run_config runs[3]; - -runs[0] = kalign_run_config_defaults(); -runs[0].gpo = 8.0f; runs[0].vsm_amax = 2.0f; runs[0].tree_noise = 0.0f; - -runs[1] = kalign_run_config_defaults(); -runs[1].gpo = 6.0f; runs[1].vsm_amax = 1.5f; runs[1].tree_noise = 0.3f; -runs[1].consistency_anchors = 5; /* different consistency per run! */ - -runs[2] = kalign_run_config_defaults(); -runs[2].gpo = 10.0f; runs[2].type = KALIGN_TYPE_CORBLOSUM; -runs[2].tree_noise = 0.5f; runs[2].tree_seed = 12345; - -struct kalign_ensemble_config ens = { .seed = 42, .min_support = 0, .save_poar = NULL }; -kalign_align_full(msa, runs, 3, &ens, 4); -``` - -### CLI mode presets (using the config structs) - -```c -/* --fast */ -struct kalign_run_config run = kalign_run_config_defaults(); -run.consistency_anchors = 0; -kalign_align_full(msa, &run, 1, NULL, n_threads); - -/* default */ -struct kalign_run_config run = kalign_run_config_defaults(); -run.consistency_anchors = 5; -kalign_align_full(msa, &run, 1, NULL, n_threads); - -/* --precise */ -struct kalign_run_config base = kalign_run_config_defaults(); -base.consistency_anchors = 5; -base.realign = 1; -struct kalign_run_config runs[3]; -kalign_generate_ensemble_runs(&base, 3, 42, runs); -struct kalign_ensemble_config ens = kalign_ensemble_config_defaults(); -kalign_align_full(msa, runs, 3, &ens, n_threads); -``` - -## Python API (Option A: single function, list of dicts) - -### pybind11 binding - -```cpp -// Single Python function that handles both single-run and ensemble -m.def("align_full", [](const std::string& input_path, - const std::string& output_path, - py::list run_configs, // list of dicts - py::dict ensemble_config, // {} for single-run - int n_threads, - const std::string& format) { - int n_runs = py::len(run_configs); - std::vector runs(n_runs); - for (int i = 0; i < n_runs; i++) { - runs[i] = kalign_run_config_defaults(); - py::dict d = run_configs[i]; - if (d.contains("gpo")) runs[i].gpo = d["gpo"].cast(); - if (d.contains("vsm_amax")) runs[i].vsm_amax = d["vsm_amax"].cast(); - // ... etc for all fields, only override what's present - } - // ... build ensemble config if n_runs > 1, call kalign_align_full -}); -``` - -### Python wrapper - -```python -def align_full(input_path, output_path, *, - runs, # list of dicts, one per run - min_support=0, - ensemble_seed=42, - save_poar=None, - expand_ensemble=None, # int: generate N runs from runs[0] using diversity table - n_threads=1, - format="fasta"): - """ - Unified alignment function. - - Args: - runs: List of run config dicts. Each dict can contain: - gpo, gpe, tgpe, vsm_amax, dist_scale, use_seq_weights, - consistency_anchors, consistency_weight, refine, adaptive_budget, - realign, tree_seed, tree_noise, type - Missing keys use defaults. - expand_ensemble: If set, takes runs[0] as base and generates - this many runs using the built-in diversity table. - Ignores runs[1:] if present. - min_support: POAR consensus threshold (0 = auto). Only for ensemble. - ensemble_seed: Base RNG seed for ensemble. Only for ensemble. - """ -``` - -### Python usage examples - -**Single run:** -```python -kalign.align_full("in.fa", "out.fa", - runs=[{"vsm_amax": 2.0, "consistency_anchors": 5}]) -``` - -**Ensemble with diversity table (easy mode):** -```python -kalign.align_full("in.fa", "out.fa", - runs=[{"vsm_amax": 2.0, "realign": 1}], - expand_ensemble=3) -``` - -**Ensemble with fully custom per-run params (optimizer):** -```python -kalign.align_full("in.fa", "out.fa", - runs=[ - {"gpo": 8.0, "vsm_amax": 2.0, "tree_noise": 0.0}, - {"gpo": 6.0, "vsm_amax": 1.5, "tree_noise": 0.3, "consistency_anchors": 5}, - {"gpo": 10.0, "type": "corblosum", "tree_noise": 0.5}, - ], - min_support=0) -``` - -### Compatibility with pymoo optimizer - -The optimizer's decision vector maps directly to the array-of-runs: - -```python -def decode_to_runs(x, n_runs): - """Map pymoo decision vector → list of run config dicts.""" - DIMS_PER_RUN = 12 # gpo, gpe, tgpe, vsm_amax, dist_scale, seq_weights, - # consistency_anchors, consistency_weight, refine, - # adaptive_budget, realign, tree_noise - runs = [] - for i in range(n_runs): - off = i * DIMS_PER_RUN - runs.append({ - "gpo": x[off + 0], - "gpe": x[off + 1], - "tgpe": x[off + 2], - "vsm_amax": x[off + 3], - "dist_scale": x[off + 4], - "use_seq_weights": x[off + 5], - "consistency_anchors": int(x[off + 6]), - "consistency_weight": x[off + 7], - "refine": int(x[off + 8]), - "adaptive_budget": int(x[off + 9]), - "realign": int(x[off + 10]), - "tree_noise": x[off + 11], - }) - return runs - -def evaluate(x, input_path, n_threads): - n_runs = int(x[60]) # from ensemble block at end of vector - runs = decode_to_runs(x, n_runs) - - # Same call regardless of n_runs - kalign.align_full(input_path, output_path, - runs=runs, - min_support=int(x[61]), - n_threads=n_threads) - - return f1, tc, wall_time # three NSGA-II objectives -``` - -**Decision vector layout** (fixed size, `max_runs=5`): -``` -┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ -│ runs[0] │ runs[1] │ runs[2] │ runs[3] │ runs[4] │ ensemble │ -│ 12 dims │ 12 dims │ 12 dims │ 12 dims │ 12 dims │ 3 dims │ -└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ - ↑ masked if n_runs < 5 -``` - -Benefits for NSGA-II: -- **No shared-vs-per-run ambiguity** — each run block is self-contained -- **Run-level crossover** — swap entire 12-dim run blocks between individuals -- **Clean masking** — unused run blocks are simply ignored -- **One code path** — evaluation function doesn't branch on mode - -## What gets removed - -| Function | Action | -|----------|--------| -| `kalign_run()` | Deprecated wrapper: `cfg = defaults(); kalign_align_full(msa, &cfg, 1, NULL, n_threads)` | -| `kalign_run_seeded()` | **Keep as internal helper** — called by `kalign_align_full` for each single run | -| `kalign_run_dist_scale()` | **Remove entirely** — redundant subset of `kalign_run_seeded` | -| `kalign_run_realign()` | **Keep as internal helper** — called by `kalign_align_full` when `realign > 0` | -| `kalign_ensemble()` | Deprecated wrapper: calls `kalign_generate_ensemble_runs` + `kalign_align_full` | -| `kalign_ensemble_custom()` | Deprecated wrapper: builds run config array + `kalign_align_full` | -| `kalign_post_realign()` | **Remove** — unused, was found to hurt performance | -| `kalign()` | Deprecated wrapper: arr→msa conversion + `kalign_align_full` | - -**Important**: `kalign_run_seeded` and `kalign_run_realign` remain as internal implementation — they do the actual alignment work. `kalign_align_full` orchestrates WHEN to call them and with WHAT parameters. - -## Sentinel handling (unified) - -All sentinel resolution happens once in `kalign_align_full()`, before passing params to internal helpers: - -```c -/* For each run config: */ -for (int k = 0; k < n_runs; k++) { - resolved[k] = runs[k]; /* copy */ - - /* Resolve vsm_amax sentinel */ - if (resolved[k].vsm_amax < 0.0f) - resolved[k].vsm_amax = (biotype == PROTEIN) ? 2.0f : 0.0f; - - /* Resolve use_seq_weights sentinel */ - if (resolved[k].use_seq_weights < 0.0f) - resolved[k].use_seq_weights = 0.0f; /* biotype default is 0.0 for all */ - - /* gpo/gpe/tgpe: -1.0 sentinel passed through to aln_param_init which handles it */ - /* dist_scale: 0.0 = off, always passed through (no sentinel) */ -} -``` - -## Ensemble post-selection refinement - -Currently hardcoded to `KALIGN_REFINE_CONFIDENT` in both ensemble functions. In the unified design, this stays hardcoded with a clear comment — it's always beneficial and changing it has not been tested. If we ever want to make it configurable, it would go into `kalign_ensemble_config`. - -## Backward compatibility - -Old function signatures remain in `kalign.h` as **deprecated wrappers** that construct run configs and call `kalign_align_full()`: - -1. Existing C code that calls `kalign_run()` or `kalign_ensemble()` continues to work unchanged. -2. The wrappers can be removed in a future major version. -3. The old `kalign()` function (char** API) remains for external consumers. - -## Testing Strategy - -### Phase 1: Verify behavioral equivalence - -For each old function, create a test that: -1. Runs alignment with the old function -2. Runs the same alignment with `kalign_align_full` using equivalent config -3. Asserts the output alignments are IDENTICAL (byte-for-byte) - -Test cases from `tests/data/`: -- BB11001 (small, protein) -- BB12006 (medium, protein) -- BB30014 (protein with insertions) - -Test matrix: -``` -kalign_run(gpo=-1, gpe=-1, tgpe=-1, refine=NONE, adaptive=0) - == kalign_align_full(&defaults, 1, NULL, n_threads) - -kalign_run_seeded(gpo=8.0, gpe=1.0, tgpe=0.5, vsm_amax=2.0, seq_weights=1.0, consistency=5) - == kalign_align_full(&{same params}, 1, NULL, n_threads) - -kalign_run_dist_scale(dist_scale=0.5, vsm_amax=2.0, seq_weights=1.0) - == kalign_align_full(&{same params, consistency=0}, 1, NULL, n_threads) - -kalign_run_realign(realign=2, consistency=5, vsm_amax=2.0) - == kalign_align_full(&{same params}, 1, NULL, n_threads) - -kalign_ensemble(n_runs=3, seed=42, refine=CONFIDENT, vsm_amax=2.0, realign=1) - == kalign_generate_ensemble_runs(&base, 3, 42, runs) + kalign_align_full(runs, 3, &ens, n_threads) - -kalign_ensemble_custom(n_runs=3, run_gpo=[...], ...) - == kalign_align_full(custom_runs, 3, &ens, n_threads) -``` - -### Phase 2: Benchmark regression check - -Run BAliBASE 218 cases with: -- Old CLI path → scores -- New `kalign_align_full` path → scores -- Assert identical F1/TC (to 6 decimal places) - -### Phase 3: Python API regression - -Run existing Python test suite (`tests/python/`). All C tests + Python tests must pass. - -## Implementation Order - -1. Add `kalign_run_config`, `kalign_ensemble_config` structs and `kalign_*_defaults()` to new header `kalign_config.h` -2. Add `kalign_generate_ensemble_runs()` — extracts the existing diversity table into a config generator -3. Implement `kalign_align_full()` in `aln_wrap.c`, calling existing `kalign_run_seeded` and `kalign_run_realign` internally -4. Move ensemble orchestration from `ensemble.c` into `kalign_align_full()` (or keep as helper called by it) -5. Write equivalence tests (Phase 1) -6. Convert CLI to use `kalign_align_full()` — verify all existing tests pass -7. Convert pybind11 to single `align_full` function — verify Python tests pass -8. Run BAliBASE regression (Phase 2) -9. Add deprecated wrappers for old functions -10. Remove `kalign_run_dist_scale` and `kalign_post_realign` -11. Expose `align_full` in Python wrapper (`__init__.py`) with `expand_ensemble` convenience param - -## Files Modified - -| File | Change | -|------|--------| -| `lib/include/kalign/kalign_config.h` | **NEW**: `kalign_run_config`, `kalign_ensemble_config`, defaults functions | -| `lib/include/kalign/kalign.h` | Add `kalign_align_full()`, `kalign_generate_ensemble_runs()`. Mark old functions deprecated. | -| `lib/src/aln_wrap.c` | Implement `kalign_align_full()`. Convert old functions to wrappers. Remove `kalign_run_dist_scale`. | -| `lib/src/ensemble.c` | Extract diversity table into `kalign_generate_ensemble_runs()`. Move orchestration to `kalign_align_full()`. | -| `src/run_kalign.c` | Replace if/else chain with config struct + `kalign_align_full()` | -| `python-kalign/_core.cpp` | Replace router + `ensemble_custom_file_to_file` with single `align_full` binding | -| `python-kalign/__init__.py` | Add `align_full()` wrapper with `expand_ensemble` convenience | -| `tests/` | Add equivalence tests | - -## Risks - -1. **Ensemble diversity table**: The old `kalign_ensemble` uses a hardcoded scale-factor table. `kalign_generate_ensemble_runs` must reproduce this exactly. We keep the table as-is and just expose it as a config generator. - -2. **Post-selection refinement**: Currently hardcodes `REFINE_CONFIDENT`. Keeping this hardcoded is safe; making it configurable would need testing. - -3. **Thread safety**: `kalign_run_config` is a plain value struct with no heap pointers. `kalign_ensemble_config` has `save_poar` (read-only string) and `seed`. Multiple threads can safely use different configs. - -4. **Binary compatibility**: Adding new functions and structs is fine. Old functions become wrappers → no ABI break. Can be removed in a future major version. - -5. **Struct size stability**: If we add fields to `kalign_run_config` later, code that uses `kalign_run_config_defaults()` gets the new defaults automatically. Code that initializes with `= {0}` or partial initialization would get zeros for new fields, which should be safe (sentinels and "off" values are 0 or -1.0). diff --git a/benchmarks/PRD_unified_optimizer.md b/benchmarks/PRD_unified_optimizer.md deleted file mode 100644 index 7757240..0000000 --- a/benchmarks/PRD_unified_optimizer.md +++ /dev/null @@ -1,421 +0,0 @@ -# PRD: Unified Parameter Optimizer - -## Goal - -Build `benchmarks/optimize_unified.py` — a single NSGA-II optimizer that searches across kalign's entire operating range: from fast single-run alignment (no consistency, no ensemble) through consistency-enhanced single-run to multi-run ensemble with POAR consensus. The optimizer always uses three objectives: F1, TC, and wall time. The resulting 3D Pareto surface reveals the full speed/accuracy trade-off landscape in one run. - -## Motivation - -Previous optimization was split into separate scripts for single-run (`optimize_params.py`) and ensemble (`optimize_ensemble.py`), each producing isolated Pareto fronts that can't be directly compared. A unified optimizer solves this: - -1. **Direct comparability**: All configurations live on the same Pareto surface. -2. **Discovery of hybrid regimes**: The optimizer might find unexpected sweet spots. -3. **One run, one answer**: Instead of running 4+ separate optimizations and manually comparing results, one run produces the complete picture. -4. **Mandatory wall time**: Time is always the 3rd objective, so the Pareto front naturally stratifies from fast/rough to slow/accurate. - -## Complete Parameter Inventory - -This section maps every lever that exists in kalign's C code, regardless of whether it's currently exposed through the Python API. The goal is to understand the full landscape before deciding what to optimize. - -### C-level parameters (from `aln_param` struct + function signatures) - -| Parameter | C field/arg | Type | Default (protein) | Where set | Description | -|-----------|------------|------|-------------------|-----------|-------------| -| `gpo` | `ap->gpo` | float | 7.0 (P43/P60), 5.5 (CB66), 55 (GON250) | `aln_param_init` | Gap open penalty | -| `gpe` | `ap->gpe` | float | 1.25 (P43/P60), 2.0 (CB66), 8 (GON250) | `aln_param_init` | Gap extend penalty | -| `tgpe` | `ap->tgpe` | float | 1.0 (P43/P60/CB66), 4 (GON250) | `aln_param_init` | Terminal gap extend penalty | -| `subm` | `ap->subm[23][23]` | float[][] | Matrix-dependent | `aln_param_init` | Substitution matrix (selected by `type`) | -| `type` | arg to `aln_param_init` | int | `PROTEIN_PFASUM43` | caller | Which substitution matrix: PFASUM43, PFASUM60, CorBLOSUM66, GON250 | -| `vsm_amax` | `ap->vsm_amax` | float | 2.0 protein, 0.0 DNA/RNA | `aln_param_init` | Variable scoring matrix: subtracts `max(0, amax-d)` from subst scores for close pairs | -| `use_seq_weights` | `ap->use_seq_weights` | float | 0.0 | `aln_param_init` | Profile rebalancing pseudocount (0=off) | -| `dist_scale` | `ap->dist_scale` | float | 0.0 | `aln_param_init` | Distance-dependent gap scaling (0=off) | -| `consistency_anchors` | `ap->consistency_anchors` | int | 0 | `kalign_run_seeded` | Number of anchor sequences K for consistency transform (0=off) | -| `consistency_weight` | `ap->consistency_weight` | float | 2.0 | `kalign_run_seeded` | Bonus scale for consistency anchors | -| `adaptive_budget` | `ap->adaptive_budget` | int | 0 | caller | Scale refinement trial count by uncertainty (0=off, 1=on) | -| `subm_offset` | `ap->subm_offset` | float | 0.0 | computed | VSM offset, computed per alignment step (not user-settable) | -| `refine` | arg | int | NONE=0 | caller | Post-alignment refinement: NONE(0), ALL(1), CONFIDENT(2), INLINE(3) | -| `realign` | arg to `kalign_run_realign` | int | 0 | caller | Alignment-guided UPGMA tree rebuild iterations (0=off) | -| `tree_seed` | arg to `kalign_run_seeded` | uint64 | 0 | caller | Random seed for noisy guide tree (0=deterministic) | -| `tree_noise` | arg to `kalign_run_seeded` | float | 0.0 | caller | Tree perturbation noise sigma (0=none) | -| `n_runs` | arg to `kalign_ensemble*` | int | 1 | caller | Number of ensemble runs (1=single-run) | -| `min_support` | arg to ensemble | int | 0 | caller | POAR consensus threshold (0=auto selection-vs-consensus) | - -### What flows where - -``` -kalign_run_seeded(): - gpo, gpe, tgpe, type (→ subst matrix) - vsm_amax, use_seq_weights, dist_scale - consistency_anchors, consistency_weight - refine, adaptive_budget - tree_seed, tree_noise - -kalign_run_realign(): - same as kalign_run_seeded EXCEPT: - - tree_seed/tree_noise NOT supported (uses alignment-derived tree) - - adds realign_iterations - - consistency is rebuilt each iteration - -kalign_ensemble_custom(): - PER-RUN: gpo[], gpe[], tgpe[], type[], noise[] - SHARED: vsm_amax, use_seq_weights, consistency_anchors, - consistency_weight, realign, refine, min_support - NOTE: use_seq_weights IS passed through to each per-run alignment. - The -1.0 sentinel defaults to 0.0, but explicit positive values - work fine. Prior finding that seq_weights "hurts ensemble" was - with hardcoded scale-factor table — worth re-exploring. -``` - -### Parameters NOT currently in the ensemble_custom API - -| Parameter | Status | Worth adding? | -|-----------|--------|--------------| -| `dist_scale` | Hardcoded to 0.0 in ensemble_custom | Low priority — VSM serves similar purpose | -| `adaptive_budget` | Hardcoded to 0 | Low priority — minor effect | -| `refine` per-run | Currently shared across all runs | Possible but complex — would need per-run refine[] array | - -### Available substitution matrices - -| `type` constant | Name | Default gaps | Origin | Notes | -|----------------|------|-------------|--------|-------| -| `KALIGN_TYPE_PROTEIN_PFASUM43` | PFASUM43 | gpo=7.0 gpe=1.25 tgpe=1.0 | Keul et al. 2017, 43% clustering | Current default | -| `KALIGN_TYPE_PROTEIN_PFASUM60` | PFASUM60 | gpo=7.0 gpe=1.25 tgpe=1.0 | Keul et al. 2017, 60% clustering | Optimization found this is best | -| `KALIGN_TYPE_PROTEIN` | CorBLOSUM66 | gpo=5.5 gpe=2.0 tgpe=1.0 | BLOSUM66 variant | Higher entropy | -| `KALIGN_TYPE_PROTEIN_DIVERGENT` | GON250 | gpo=55 gpe=8 tgpe=4 | Gonnet 1992 | Very different scale, for divergent seqs | - -## Decisions for the Unified Optimizer - -### What to optimize - -| Parameter | Include? | Rationale | -|-----------|----------|-----------| -| `n_runs` | YES {1, 3, 5} | Core mode variable | -| `gpo_i` per-run | YES [2, 15] | Per-run diversity is key for ensemble | -| `gpe_i` per-run | YES [0.5, 5] | Per-run diversity | -| `tgpe_i` per-run | YES [0.1, 3] | Per-run diversity | -| `noise_i` per-run | YES [0, 0.5] | Tree perturbation per-run | -| `matrix_i` per-run | YES {P43, P60, CB66} | Matrix diversity per-run | -| `vsm_amax` shared | YES [0, 5] | Major effect on quality | -| `seq_weights` shared | YES [0, 5] | Works for single-run; *also* passed to per-run alignments in ensemble — re-explore | -| `consistency` shared | YES {0, 1, 2, 3, 5, 8, 10} | Huge effect, expensive | -| `consistency_weight` shared | YES [0.5, 5] | Tunes consistency strength | -| `realign` shared | YES {0, 1, 2} | Alignment-guided tree rebuild | -| `refine` shared | YES {NONE, CONFIDENT, ALL, INLINE} | Post-alignment refinement mode | -| `min_support` shared | YES {0, 1, ..., max_runs} | POAR consensus threshold | -| `dist_scale` | NO | Largely redundant with VSM, 0 extra dims | -| `adaptive_budget` | NO | Minor effect, adds noise to search | -| GON250 matrix | NO | Very different scale, would need separate gap ranges | - -### Refine as a searchable parameter - -Currently refine is hardcoded: -- Single-run: not used (NONE) -- Ensemble: REFINE_CONFIDENT for post-selection refinement - -But refine={NONE, CONFIDENT, ALL, INLINE} could be optimized. INLINE is particularly interesting — it does refinement *during* progressive alignment rather than as a post-step. This has never been benchmarked in combination with consistency or ensemble. - -Encode as discrete: {0=NONE, 1=ALL, 2=CONFIDENT, 3=INLINE} - -### Masking rules (which params are inactive when) - -The masking logic forces inactive parameters to neutral values during decode, so mutations in those dimensions are silent: - -| Condition | Masked parameters | Forced value | Why | -|-----------|-------------------|-------------|-----| -| `n_runs == 1` | `noise_0` | 0.0 | No tree perturbation in single-run path (uses deterministic tree) | -| `n_runs == 1` | `min_support` | 0 | No POAR consensus | -| `n_runs == 1` | run_1..N params | Copy of run_0 | Dead dimensions | -| `n_runs > 1` | — | — | **All shared params remain active**, including `seq_weights` | -| `consistency == 0` | `consistency_weight` | 1.0 | No anchors → weight irrelevant | -| `realign > 0` | `noise_i` | 0.0 | `kalign_run_realign` doesn't use tree_seed/noise (uses alignment-derived tree) | - -**Important**: `seq_weights` is NOT masked for ensemble mode. The C code passes `use_seq_weights` through to each per-run alignment via `kalign_run_seeded`/`kalign_run_realign`. The previous finding that "seq_weights hurts ensemble" was with the old hardcoded parameters. With optimized per-run params, seq_weights might help. The optimizer should be free to explore this. - -**Important**: When `realign > 0`, the ensemble path calls `kalign_run_realign` which builds a deterministic alignment-derived tree. Tree noise has no effect in this path. So if the optimizer picks `realign > 0`, per-run noise values should be masked to 0. However, this creates a complex interaction: `realign=0` enables noise diversity, `realign>0` gives better trees but loses noise diversity. The optimizer should discover which trade-off wins. - -### C API changes needed - -The current `ensemble_custom_file_to_file` Python binding needs one addition: - -1. **`refine` parameter**: Currently hardcoded to `REFINE_CONFIDENT` in the optimize_ensemble.py evaluation. The binding already accepts it as a parameter. We just need to make it a decision variable instead of hardcoding. - -No C code changes needed. All the levers already exist. - -## Parameter Space (final) - -### Per-run parameters (allocated for max_runs slots) - -| Parameter | Range | Type | Dims per run | -|-----------|-------|------|-------------| -| `gpo_i` | [2.0, 15.0] | continuous | 1 | -| `gpe_i` | [0.5, 5.0] | continuous | 1 | -| `tgpe_i` | [0.1, 3.0] | continuous | 1 | -| `noise_i` | [0.0, 0.5] | continuous | 1 | -| `matrix_i` | {0, 1, 2} | discrete | 1 | - -= 5 per run × max_runs - -### Shared parameters - -| Parameter | Range | Type | Dims | -|-----------|-------|------|------| -| `vsm_amax` | [0.0, 5.0] | continuous | 1 | -| `seq_weights` | [0.0, 5.0] | continuous | 1 | -| `consistency_weight` | [0.5, 5.0] | continuous | 1 | -| `n_runs` | {0, 1, 2} for max5, {0, 1, 2, 3} for max8 | discrete | 1 | -| `consistency` | {0..6} → [0, 1, 2, 3, 5, 8, 10] | discrete | 1 | -| `realign` | {0, 1, 2} | discrete | 1 | -| `refine` | {0, 1, 2, 3} → [NONE, ALL, CONFIDENT, INLINE] | discrete | 1 | -| `min_support` | {0..max_runs} | discrete | 1 | - -= 8 shared (3 continuous + 5 discrete) - -### Total dimensions - -| max_runs | Per-run | Shared | Total | Recommended pop_size | -|----------|---------|--------|-------|---------------------| -| 5 | 25 | 8 | 33 | 200 | -| 8 | 40 | 8 | 48 | 300 | - -## Evaluation Pipeline - -### Decision routing - -```python -def evaluate_unified(params, cases, n_threads=1, quiet=True): - n_runs = params["n_runs"] - - if n_runs == 1: - # Single-run path — full parameter control - kalign.align_file_to_file( - input, output, - gap_open=params["gpo"][0], - gap_extend=params["gpe"][0], - terminal_gap_extend=params["tgpe"][0], - seq_type=matrix_api_name(params["matrix"][0]), - vsm_amax=params["vsm_amax"], - seq_weights=params["seq_weights"], - consistency=params["consistency"], - consistency_weight=params["consistency_weight"], - realign=params["realign"], - refine=refine_api_name(params["refine"]), - ) - else: - # Ensemble path — per-run arrays - ensemble_custom_file_to_file( - input, output, - run_gpo=params["gpo"][:n_runs], - run_gpe=params["gpe"][:n_runs], - run_tgpe=params["tgpe"][:n_runs], - run_noise=params["noise"][:n_runs], - run_types=params["matrix"][:n_runs], - vsm_amax=params["vsm_amax"], - seq_weights=params["seq_weights"], # NOT forced to 0! - realign=params["realign"], - consistency_anchors=params["consistency"], - consistency_weight=params["consistency_weight"], - refine=params["refine"], - min_support=params["min_support"], - seed=42, - ) -``` - -### CV evaluation - -Same as existing: stratified k-fold (default 5) on BAliBASE 218 cases. - -## Objectives (always 3) - -1. **Maximize F1** — category-averaged, held-out CV folds → pymoo minimizes -F1 -2. **Maximize TC** — category-averaged, held-out CV folds → pymoo minimizes -TC -3. **Minimize wall time** — total CV evaluation time in seconds - -No `--optimize-time` flag. Wall time is always the 3rd objective. - -## Baselines - -Computed at startup (parallelized across folds + workers): - -| Baseline | Description | Expected profile | -|----------|-------------|-----------------| -| **fast** | n_runs=1, consistency=0, realign=0, refine=NONE, PFASUM60, default gaps | F1~0.72, TC~0.47, ~10s | -| **accurate** | n_runs=1, consistency=8, realign=2, refine=NONE, optimized gaps from run 1 | F1~0.76, TC~0.47, ~180s | -| **ensemble** | n_runs=3, consistency=0, realign=1, refine=CONFIDENT, vsm=2.0 | F1~0.77, TC~0.47, ~193s | - -## Parallelism - -### Fine-grained (individual x fold) job distribution - -Same as updated `optimize_params.py`: - -- Each job = one (individual, fold) pair, running ~44 cases -- For pop=200, k=5 → 1000 jobs per generation -- Distributed across N workers - -### Baseline parallelization - -All baseline evaluations (3 baselines × (k folds + 1 full)) run in parallel at startup. - -### Top-level function - -```python -def _eval_one_unified_fold(args_tuple): - ind_idx, fold_idx, x, test_cases, n_threads, max_runs = args_tuple - params = decode_unified_params(x, max_runs) - result = evaluate_unified(params, test_cases, n_threads, quiet=True) - return ind_idx, fold_idx, params, result -``` - -## Dashboard - -### Layout - -``` -┌─ Progress ──────────────────────────────────────────────────────┐ -│ Gen 12/100 Eval 480/20000 Elapsed 2.1h ETA 5.3h │ -│ Gen time 128s Workers 56 │ -├─ Baselines ─────────────────────────────────────────────────────┤ -│ fast: F1=0.7160 TC=0.4660 Time=10s (single, c=0) │ -│ accurate: F1=0.7557 TC=0.4713 Time=180s (single, c=8) │ -│ ensemble: F1=0.7680 TC=0.4670 Time=193s (ens3, c=0) │ -├─ Best by Objective ─────────────────────────────────────────────┤ -│ Best F1: 0.7823 (+0.0143 vs accurate) ens3 c=5 re=1 210s │ -│ Best TC: 0.5102 (+0.0389 vs accurate) ens5 c=3 re=2 340s │ -│ Fastest: 8.2s (-1.8s vs fast) single c=0 re=0 │ -├─ Pareto Front (top 12 by F1) ──────────────────────────────────┤ -│ # │ Mode │ CV F1 │ CV TC │ Time │ c re ref│ Key params │ -│ 0 │ ens5 │ 0.7823 │ 0.5001 │ 340s │ 5 1 C │ vsm=1.4 │ -│ 1 │ ens3 │ 0.7791 │ 0.4891 │ 210s │ 3 1 C │ vsm=1.8 │ -│ 2 │ ens3 │ 0.7756 │ 0.4812 │ 155s │ 0 1 C │ vsm=2.0 │ -│ 3 │ single│ 0.7557 │ 0.4713 │ 180s │ 8 2 N │ vsm=1.4 P60 │ -│ 4 │ single│ 0.7401 │ 0.4650 │ 42s │ 3 1 N │ vsm=2.0 P60 │ -│ 5 │ single│ 0.7160 │ 0.4580 │ 9s │ 0 0 N │ vsm=1.8 P60 │ -├─ Mode Distribution ────────────────────────────────────────────┤ -│ Pareto: single=8 ens3=12 ens5=5 │ -│ Pop: single=65 ens3=82 ens5=53 │ -├─ Trend ────────────────────────────────────────────────────────┤ -│ Gen 1: F1=0.7012 Gen 5: F1=0.7434 Gen 10: F1=0.7689 │ -├─ Recent ───────────────────────────────────────────────────────┤ -│ F1=0.7654 TC=0.4512 t=145s ens3 c=3 re=1 ref=C vsm=1.8 │ -│ F1=0.7201 TC=0.4380 t=12s single c=0 re=0 ref=N vsm=2.1 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Key dashboard features - -1. **Mode column**: "single", "ens3", "ens5", "ens8" — most important column. -2. **Refine column**: N=None, A=All, C=Confident, I=Inline — compact 1-char code. -3. **Mode distribution panel**: Pareto and population counts per mode. -4. **Three baselines**: Context for all three speed regimes. -5. **Time always shown**: Every entry shows wall time. -6. **Compact params**: For ensemble, show shared params only (c, re, ref, vsm, sw). Full per-run details go to output file. - -## CLI Interface - -```bash -# Quick smoke test -uv run python -m benchmarks.optimize_unified --pop-size 20 --n-gen 5 - -# Production run on Threadripper (max 5 ensemble runs) -uv run python -m benchmarks.optimize_unified \ - --pop-size 200 --n-gen 100 \ - --n-workers 56 --n-threads 1 - -# Larger search (max 8 ensemble runs) -uv run python -m benchmarks.optimize_unified \ - --max-runs 8 --pop-size 300 --n-gen 120 \ - --n-workers 56 - -# Resume -uv run python -m benchmarks.optimize_unified \ - --resume benchmarks/results/unified_optim/gen_checkpoint.pkl \ - --n-gen 150 --n-workers 56 -``` - -### Arguments - -| Argument | Default | Description | -|----------|---------|-------------| -| `--max-runs` | 5 | Max ensemble runs. Sets n_runs choices: {1,3,5} or {1,3,5,8} | -| `--pop-size` | 200 | NSGA-II population size | -| `--n-gen` | 100 | Total generations | -| `--n-folds` | 5 | Stratified CV folds | -| `--n-threads` | 1 | OpenMP threads per alignment | -| `--n-workers` | 1 | Parallel worker processes | -| `--seed` | 42 | Random seed | -| `--output-dir` | `benchmarks/results/unified_optim` | Output directory | -| `--run-name` | None | Subdirectory name | -| `--no-dashboard` | false | Disable rich dashboard | -| `--resume` | None | Checkpoint file path | - -## Checkpoint / Resume - -Same pattern as existing optimizers: -- Saves `pop_X`, `pop_F`, `n_gen_completed`, history, plus `max_runs` for validation -- Atomic write via temp file + rename -- On resume: validates `max_runs`, `n_folds`, `seed` match - -## Clean Interrupt - -- `_kill_pool()` sends SIGTERM to workers -- `os._exit(1)` to skip atexit join-hangs - -## Output Files - -1. **`gen_checkpoint.pkl`** — per-generation checkpoint -2. **`pareto_front.txt`** — human-readable with full per-run params for ensemble solutions -3. **`optim_results.pkl`** — full results pickle - -### pareto_front.txt format - -``` -# Unified kalign optimization (NSGA-II, 3 objectives: F1, TC, time) -# pop_size=200 n_gen=100 max_runs=5 n_folds=5 seed=42 -# Baselines: -# fast: F1=0.7160 TC=0.4660 Time=10s -# accurate: F1=0.7557 TC=0.4713 Time=180s -# ensemble: F1=0.7680 TC=0.4670 Time=193s - -[0] mode=ens5 CV_F1=0.7823 CV_TC=0.5001 Time=340s - n_runs=5 - vsm_amax=1.359 seq_weights=0.8 - consistency=5 consistency_weight=1.17 - realign=1 refine=CONFIDENT - min_support=2 - run_0: gpo=7.0 gpe=0.55 tgpe=0.41 noise=0.10 PFASUM60 - run_1: gpo=3.5 gpe=1.20 tgpe=0.80 noise=0.25 PFASUM43 - ... - -[1] mode=single CV_F1=0.7557 CV_TC=0.4713 Time=180s - n_runs=1 - gap_open=8.472 gap_extend=0.554 terminal_gap_extend=0.409 - vsm_amax=1.359 seq_weights=3.407 - consistency=8 consistency_weight=1.167 - realign=2 refine=NONE - matrix=PFASUM60 -``` - -## Post-Optimization Analysis - -1. Full Pareto front as rich table -2. Group by mode, show best F1/TC/time per mode -3. Re-evaluate top-3 on full dataset -4. Per-category breakdown (RV11-RV50) -5. Overfit check (full > CV by >0.02) -6. Print recommended settings for three tiers: - - **Fast** (best F1 among solutions under 15s) - - **Default** (best F1 among solutions under 60s) - - **Accurate** (best F1 overall) - -## Implementation Order - -1. Parameter space definition + encode/decode with masking -2. `evaluate_unified()` with single-run / ensemble routing -3. `_eval_one_unified_fold()` for parallel eval -4. `UnifiedCVProblem` with 3 mandatory objectives -5. Dashboard with mode columns, multiple baselines, mode distribution -6. `GenerationCallback` with checkpoint saving -7. `main()` with CLI, parallel baselines, optimization loop, results -8. Resume logic with max_runs validation -9. Post-optimization analysis -10. Smoke test: `--pop-size 10 --n-gen 3 --max-runs 5 --n-workers 4` diff --git a/benchmarks/analysis.py b/benchmarks/analysis.py deleted file mode 100644 index 4302aea..0000000 --- a/benchmarks/analysis.py +++ /dev/null @@ -1,574 +0,0 @@ -"""RV11 alignment structure analysis. - -Compares gap structure and alignment geometry between kalign, reference, -and external tools (mafft, muscle, clustalo) to understand HOW alignments -differ structurally — not just score differences. - -Usage: - # Locally (kalign only, external tools skipped if not installed): - uv run python -m benchmarks.analysis - - # Inside container (has mafft/muscle/clustalo): - python -m benchmarks.analysis - - # Specific dataset: - python -m benchmarks.analysis --dataset balibase_RV11 - - # Write CSV: - python -m benchmarks.analysis --csv benchmarks/results/gap_analysis.csv -""" - -import argparse -import csv -import json -import re -import statistics -import sys -import tempfile -from dataclasses import dataclass, fields -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -from .datasets import balibase_cases, balibase_is_available - -RESULTS_DIR = Path(__file__).parent / "results" - - -# --------------------------------------------------------------------------- -# MSF parser (reference alignments are in GCG MSF format) -# --------------------------------------------------------------------------- - -def parse_msf(path: Path) -> Dict[str, str]: - """Parse a GCG MSF file into {name: aligned_sequence}.""" - text = path.read_text() - - # Split at "//" separator - parts = text.split("//") - if len(parts) < 2: - raise ValueError(f"No // separator found in {path}") - - body = parts[1] - seqs: Dict[str, List[str]] = {} - for line in body.splitlines(): - line = line.strip() - if not line: - continue - tokens = line.split() - if len(tokens) < 2: - continue - name = tokens[0] - # Sequence characters (may contain dots for gaps) - seq_parts = "".join(tokens[1:]) - seqs.setdefault(name, []).append(seq_parts) - - # Join blocks and normalise: dots → dashes, remove whitespace - result = {} - for name, blocks in seqs.items(): - seq = "".join(blocks).replace(".", "-").upper() - result[name] = seq - return result - - -# --------------------------------------------------------------------------- -# FASTA parser (kalign/tool outputs) -# --------------------------------------------------------------------------- - -def parse_fasta(path: Path) -> Dict[str, str]: - """Parse a FASTA file into {name: sequence}.""" - seqs: Dict[str, str] = {} - current = None - parts: List[str] = [] - for line in path.read_text().splitlines(): - line = line.strip() - if line.startswith(">"): - if current is not None: - seqs[current] = "".join(parts).upper() - current = line[1:].split()[0] - parts = [] - elif current is not None: - parts.append(line) - if current is not None: - seqs[current] = "".join(parts).upper() - return seqs - - -# --------------------------------------------------------------------------- -# Gap structure metrics -# --------------------------------------------------------------------------- - -@dataclass -class GapStats: - """Gap structure metrics for one alignment.""" - n_seqs: int - alignment_length: int - mean_seq_length: float # unaligned (non-gap chars) - expansion_factor: float # alignment_length / mean_seq_length - total_gaps: int - gap_fraction: float # total_gaps / (n_seqs * alignment_length) - n_gap_blocks: int - mean_gap_block_len: float - mean_terminal_gap: float # average leading+trailing gap per sequence - mean_internal_gap: float # average total internal gap chars per sequence - n_gappy_columns: int # columns where >50% of sequences have a gap - gappy_column_fraction: float - - -def compute_gap_stats(seqs: Dict[str, str]) -> GapStats: - """Compute gap structure metrics from aligned sequences.""" - sequences = list(seqs.values()) - n_seqs = len(sequences) - if n_seqs == 0: - return GapStats(0, 0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0.0) - - aln_len = len(sequences[0]) - # Unaligned lengths (non-gap characters) - ungapped_lens = [len(s.replace("-", "")) for s in sequences] - mean_seq_len = statistics.mean(ungapped_lens) - expansion = aln_len / mean_seq_len if mean_seq_len > 0 else 0.0 - - total_gaps = sum(s.count("-") for s in sequences) - total_chars = n_seqs * aln_len - gap_frac = total_gaps / total_chars if total_chars > 0 else 0.0 - - # Gap blocks and lengths - all_block_lens: List[int] = [] - terminal_gaps: List[int] = [] - internal_gaps: List[int] = [] - - for seq in sequences: - # Find all gap blocks - blocks = [(m.start(), m.end()) for m in re.finditer(r"-+", seq)] - all_block_lens.extend(m.end() - m.start() for m in re.finditer(r"-+", seq)) - - # Terminal: leading and trailing - leading = len(seq) - len(seq.lstrip("-")) - trailing = len(seq) - len(seq.rstrip("-")) - terminal_gaps.append(leading + trailing) - - # Internal: everything that's not leading/trailing - internal = sum(e - s for s, e in blocks) - internal -= leading + trailing - internal_gaps.append(max(0, internal)) - - n_gap_blocks = len(all_block_lens) - mean_block_len = statistics.mean(all_block_lens) if all_block_lens else 0.0 - mean_terminal = statistics.mean(terminal_gaps) - mean_internal = statistics.mean(internal_gaps) - - # Gappy columns (>50% gaps) - n_gappy = 0 - for col in range(aln_len): - gaps_in_col = sum(1 for s in sequences if s[col] == "-") - if gaps_in_col > n_seqs / 2: - n_gappy += 1 - - return GapStats( - n_seqs=n_seqs, - alignment_length=aln_len, - mean_seq_length=mean_seq_len, - expansion_factor=expansion, - total_gaps=total_gaps, - gap_fraction=gap_frac, - n_gap_blocks=n_gap_blocks, - mean_gap_block_len=mean_block_len, - mean_terminal_gap=mean_terminal, - mean_internal_gap=mean_internal, - n_gappy_columns=n_gappy, - gappy_column_fraction=n_gappy / aln_len if aln_len > 0 else 0.0, - ) - - -# --------------------------------------------------------------------------- -# Alignment generation helpers -# --------------------------------------------------------------------------- - -def _align_kalign(unaligned: Path, output: Path, seq_type: str) -> None: - """Run kalign via Python API.""" - import kalign - kalign.align_file_to_file( - str(unaligned), str(output), format="fasta", seq_type=seq_type, - ) - - -def _align_external(unaligned: Path, output: Path, tool: str) -> bool: - """Run an external tool. Returns True if successful.""" - import shutil - import subprocess - - if shutil.which(tool) is None: - return False - - try: - if tool == "mafft": - with open(output, "w") as f: - subprocess.run( - ["mafft", "--auto", str(unaligned)], - stdout=f, stderr=subprocess.PIPE, check=True, - ) - elif tool == "clustalo": - subprocess.run( - ["clustalo", "-i", str(unaligned), "-o", str(output), - "--outfmt=fasta", "--force"], - capture_output=True, check=True, - ) - elif tool == "muscle": - subprocess.run( - ["muscle", "-align", str(unaligned), "-output", str(output)], - capture_output=True, check=True, - ) - return True - except (subprocess.CalledProcessError, FileNotFoundError): - return False - - -# --------------------------------------------------------------------------- -# Per-case analysis row -# --------------------------------------------------------------------------- - -@dataclass -class CaseRow: - family: str - method: str - # Scores from results JSON (NaN if not available) - recall: float - precision: float - f1: float - tc: float - # Gap stats - alignment_length: int - expansion_factor: float - gap_fraction: float - n_gap_blocks: int - mean_gap_block_len: float - mean_terminal_gap: float - mean_internal_gap: float - n_gappy_columns: int - gappy_column_fraction: float - - -# --------------------------------------------------------------------------- -# Load frozen scores from full_comparison.json -# --------------------------------------------------------------------------- - -def load_scores(json_path: Path, dataset_filter: str = "balibase_RV11") -> Dict[Tuple[str, str], dict]: - """Load {(family, method_key): scores} from results JSON. - - method_key is 'kalign' for python_api/refine=none, or the external tool name. - """ - with open(json_path) as f: - data = json.load(f) - - scores: Dict[Tuple[str, str], dict] = {} - for r in data["results"]: - if dataset_filter and r["dataset"] != dataset_filter: - continue - - if r["method"] == "python_api" and r["refine"] == "none": - key = (r["family"], "kalign") - elif r["method"] in ("clustalo", "mafft", "muscle"): - key = (r["family"], r["method"]) - else: - continue - - scores[key] = { - "recall": r.get("recall", float("nan")), - "precision": r.get("precision", float("nan")), - "f1": r.get("f1", float("nan")), - "tc": r.get("tc", float("nan")), - } - return scores - - -# --------------------------------------------------------------------------- -# Main analysis -# --------------------------------------------------------------------------- - -def analyse_rv11( - dataset_filter: str = "balibase_RV11", - external_tools: Optional[List[str]] = None, -) -> List[CaseRow]: - """Run gap analysis on all RV11 cases. Returns list of CaseRow.""" - if external_tools is None: - external_tools = ["mafft", "muscle", "clustalo"] - - if not balibase_is_available(): - print("BAliBASE not downloaded. Run: uv run python -m benchmarks --download-only") - return [] - - cases = [c for c in balibase_cases() if c.dataset == dataset_filter] - if not cases: - print(f"No cases found for {dataset_filter}") - return [] - - # Load frozen scores - json_path = RESULTS_DIR / "full_comparison.json" - scores = load_scores(json_path, dataset_filter) if json_path.exists() else {} - - rows: List[CaseRow] = [] - - for case in cases: - print(f" {case.family} ...", end="", flush=True) - - # --- Reference alignment --- - ref_seqs = parse_msf(case.reference) - ref_stats = compute_gap_stats(ref_seqs) - sc = scores.get((case.family, "reference"), {}) - rows.append(CaseRow( - family=case.family, method="reference", - recall=1.0, precision=1.0, f1=1.0, tc=1.0, - alignment_length=ref_stats.alignment_length, - expansion_factor=ref_stats.expansion_factor, - gap_fraction=ref_stats.gap_fraction, - n_gap_blocks=ref_stats.n_gap_blocks, - mean_gap_block_len=ref_stats.mean_gap_block_len, - mean_terminal_gap=ref_stats.mean_terminal_gap, - mean_internal_gap=ref_stats.mean_internal_gap, - n_gappy_columns=ref_stats.n_gappy_columns, - gappy_column_fraction=ref_stats.gappy_column_fraction, - )) - - # --- Kalign --- - with tempfile.TemporaryDirectory() as tmpdir: - kalign_out = Path(tmpdir) / f"{case.family}_kalign.fa" - _align_kalign(case.unaligned, kalign_out, case.seq_type) - kalign_seqs = parse_fasta(kalign_out) - kalign_stats = compute_gap_stats(kalign_seqs) - - sc = scores.get((case.family, "kalign"), {}) - rows.append(CaseRow( - family=case.family, method="kalign", - recall=sc.get("recall", float("nan")), - precision=sc.get("precision", float("nan")), - f1=sc.get("f1", float("nan")), - tc=sc.get("tc", float("nan")), - alignment_length=kalign_stats.alignment_length, - expansion_factor=kalign_stats.expansion_factor, - gap_fraction=kalign_stats.gap_fraction, - n_gap_blocks=kalign_stats.n_gap_blocks, - mean_gap_block_len=kalign_stats.mean_gap_block_len, - mean_terminal_gap=kalign_stats.mean_terminal_gap, - mean_internal_gap=kalign_stats.mean_internal_gap, - n_gappy_columns=kalign_stats.n_gappy_columns, - gappy_column_fraction=kalign_stats.gappy_column_fraction, - )) - - # --- External tools --- - for tool in external_tools: - with tempfile.TemporaryDirectory() as tmpdir: - tool_out = Path(tmpdir) / f"{case.family}_{tool}.fa" - ok = _align_external(case.unaligned, tool_out, tool) - if not ok: - continue - tool_seqs = parse_fasta(tool_out) - tool_stats = compute_gap_stats(tool_seqs) - - sc = scores.get((case.family, tool), {}) - rows.append(CaseRow( - family=case.family, method=tool, - recall=sc.get("recall", float("nan")), - precision=sc.get("precision", float("nan")), - f1=sc.get("f1", float("nan")), - tc=sc.get("tc", float("nan")), - alignment_length=tool_stats.alignment_length, - expansion_factor=tool_stats.expansion_factor, - gap_fraction=tool_stats.gap_fraction, - n_gap_blocks=tool_stats.n_gap_blocks, - mean_gap_block_len=tool_stats.mean_gap_block_len, - mean_terminal_gap=tool_stats.mean_terminal_gap, - mean_internal_gap=tool_stats.mean_internal_gap, - n_gappy_columns=tool_stats.n_gappy_columns, - gappy_column_fraction=tool_stats.gappy_column_fraction, - )) - - print(" done") - - return rows - - -# --------------------------------------------------------------------------- -# Output formatting -# --------------------------------------------------------------------------- - -def print_table(rows: List[CaseRow]) -> None: - """Print a summary table to stdout, grouped by family.""" - if not rows: - return - - families = sorted(set(r.family for r in rows)) - methods = sorted(set(r.method for r in rows)) - - # Per-case comparison table - hdr = f"{'Family':<10} {'Method':<10} {'Recall':>7} {'Prec':>7} {'F1':>7} {'AlnLen':>7} {'Expand':>7} {'GapFrac':>7} {'GapBlk':>7} {'MeanBL':>7} {'TermGap':>7} {'IntGap':>7} {'Gappy%':>7}" - print("\n" + "=" * len(hdr)) - print(hdr) - print("-" * len(hdr)) - - for fam in families: - fam_rows = sorted( - [r for r in rows if r.family == fam], - key=lambda r: (r.method != "reference", r.method != "kalign", r.method), - ) - for r in fam_rows: - rec = f"{r.recall:.3f}" if r.recall == r.recall else " n/a" - pre = f"{r.precision:.3f}" if r.precision == r.precision else " n/a" - f1 = f"{r.f1:.3f}" if r.f1 == r.f1 else " n/a" - print( - f"{r.family:<10} {r.method:<10} {rec:>7} {pre:>7} {f1:>7} " - f"{r.alignment_length:>7} {r.expansion_factor:>7.2f} " - f"{r.gap_fraction:>7.3f} {r.n_gap_blocks:>7} " - f"{r.mean_gap_block_len:>7.1f} {r.mean_terminal_gap:>7.1f} " - f"{r.mean_internal_gap:>7.1f} {r.gappy_column_fraction:>7.3f}" - ) - print() - - # Aggregate summary by method - print("=" * 80) - print("AGGREGATE SUMMARY (means across all families)") - print("-" * 80) - fmt = "{:<10} {:>7} {:>7} {:>7} {:>8} {:>7} {:>7} {:>8} {:>8}" - print(fmt.format("Method", "Recall", "Prec", "F1", "Expand", "GapFrac", "MeanBL", "TermGap", "IntGap")) - print("-" * 80) - - for method in ["reference", "kalign"] + [m for m in methods if m not in ("reference", "kalign")]: - method_rows = [r for r in rows if r.method == method] - if not method_rows: - continue - def safe_mean(vals): - clean = [v for v in vals if v == v] # filter NaN - return statistics.mean(clean) if clean else float("nan") - - rec = safe_mean([r.recall for r in method_rows]) - pre = safe_mean([r.precision for r in method_rows]) - f1 = safe_mean([r.f1 for r in method_rows]) - exp = statistics.mean([r.expansion_factor for r in method_rows]) - gf = statistics.mean([r.gap_fraction for r in method_rows]) - mbl = statistics.mean([r.mean_gap_block_len for r in method_rows]) - tg = statistics.mean([r.mean_terminal_gap for r in method_rows]) - ig = statistics.mean([r.mean_internal_gap for r in method_rows]) - - rec_s = f"{rec:.3f}" if rec == rec else " n/a" - pre_s = f"{pre:.3f}" if pre == pre else " n/a" - f1_s = f"{f1:.3f}" if f1 == f1 else " n/a" - print(fmt.format(method, rec_s, pre_s, f1_s, f"{exp:.2f}", f"{gf:.3f}", f"{mbl:.1f}", f"{tg:.1f}", f"{ig:.1f}")) - - # Correlation analysis: precision vs expansion factor for kalign - kalign_rows = [r for r in rows if r.method == "kalign" and r.precision == r.precision] - if len(kalign_rows) >= 5: - print("\n" + "=" * 80) - print("CORRELATION: kalign precision vs gap metrics") - print("-" * 80) - - # Simple Pearson correlation - def pearson(xs, ys): - n = len(xs) - if n < 3: - return float("nan") - mx, my = statistics.mean(xs), statistics.mean(ys) - num = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) - dx = sum((x - mx) ** 2 for x in xs) ** 0.5 - dy = sum((y - my) ** 2 for y in ys) ** 0.5 - return num / (dx * dy) if dx > 0 and dy > 0 else float("nan") - - prec = [r.precision for r in kalign_rows] - metrics = [ - ("expansion_factor", [r.expansion_factor for r in kalign_rows]), - ("gap_fraction", [r.gap_fraction for r in kalign_rows]), - ("mean_gap_block_len", [r.mean_gap_block_len for r in kalign_rows]), - ("mean_terminal_gap", [r.mean_terminal_gap for r in kalign_rows]), - ("mean_internal_gap", [r.mean_internal_gap for r in kalign_rows]), - ("gappy_column_fraction", [r.gappy_column_fraction for r in kalign_rows]), - ] - for name, vals in metrics: - r = pearson(prec, vals) - print(f" precision vs {name:<25}: r = {r:+.3f}") - - # Also: kalign expansion vs reference expansion - ref_rows = {r.family: r for r in rows if r.method == "reference"} - kalign_dict = {r.family: r for r in kalign_rows} - common = sorted(set(ref_rows) & set(kalign_dict)) - if common: - print(f"\n Expansion factor comparison (kalign vs reference, n={len(common)}):") - over = [f for f in common if kalign_dict[f].expansion_factor > ref_rows[f].expansion_factor * 1.05] - under = [f for f in common if kalign_dict[f].expansion_factor < ref_rows[f].expansion_factor * 0.95] - same = [f for f in common if f not in over and f not in under] - print(f" Over-expanded (>5%): {len(over)}") - print(f" Under-expanded: {len(under)}") - print(f" Similar (+/-5%): {len(same)}") - - # Relative expansion ratio correlated with precision - ratios = [kalign_dict[f].expansion_factor / max(ref_rows[f].expansion_factor, 0.01) for f in common] - precs = [kalign_dict[f].precision for f in common] - r_ratio = pearson(ratios, precs) - print(f" Correlation (expand_ratio vs precision): r = {r_ratio:+.3f}") - - if over: - print(f"\n Worst over-expanded cases (kalign expand / ref expand):") - ranked = sorted(over, key=lambda f: kalign_dict[f].expansion_factor / max(ref_rows[f].expansion_factor, 0.01), reverse=True) - for f in ranked[:10]: - ke = kalign_dict[f].expansion_factor - re = ref_rows[f].expansion_factor - kp = kalign_dict[f].precision - print(f" {f}: kalign={ke:.2f} ref={re:.2f} ratio={ke/re:.2f} precision={kp:.3f}") - - if under: - print(f"\n Worst under-expanded cases (kalign expand / ref expand):") - ranked = sorted(under, key=lambda f: kalign_dict[f].expansion_factor / max(ref_rows[f].expansion_factor, 0.01)) - for f in ranked[:10]: - ke = kalign_dict[f].expansion_factor - re = ref_rows[f].expansion_factor - kp = kalign_dict[f].precision - print(f" {f}: kalign={ke:.2f} ref={re:.2f} ratio={ke/re:.2f} precision={kp:.3f}") - - -def write_csv(rows: List[CaseRow], path: str) -> None: - """Write analysis results to CSV.""" - fieldnames = [f.name for f in fields(CaseRow)] - Path(path).parent.mkdir(parents=True, exist_ok=True) - with open(path, "w", newline="") as f: - writer = csv.DictWriter(f, fieldnames=fieldnames) - writer.writeheader() - for row in rows: - d = {fn: getattr(row, fn) for fn in fieldnames} - writer.writerow(d) - print(f"\nCSV written to {path}") - - -# --------------------------------------------------------------------------- -# CLI entry point -# --------------------------------------------------------------------------- - -def main() -> None: - parser = argparse.ArgumentParser( - description="RV11 alignment structure analysis", - prog="python -m benchmarks.analysis", - ) - parser.add_argument( - "--dataset", default="balibase_RV11", - help="Dataset filter (default: balibase_RV11)", - ) - parser.add_argument( - "--csv", default="", - help="Write results to CSV file", - ) - parser.add_argument( - "--no-external", action="store_true", - help="Skip external tools (mafft, muscle, clustalo)", - ) - args = parser.parse_args() - - external = [] if args.no_external else ["mafft", "muscle", "clustalo"] - - print(f"Analysing {args.dataset} alignment structure...") - rows = analyse_rv11(dataset_filter=args.dataset, external_tools=external) - - if not rows: - sys.exit(1) - - print_table(rows) - - if args.csv: - write_csv(rows, args.csv) - - -if __name__ == "__main__": - main() diff --git a/benchmarks/app.py b/benchmarks/app.py deleted file mode 100644 index 86e4cdb..0000000 --- a/benchmarks/app.py +++ /dev/null @@ -1,603 +0,0 @@ -"""Dash app for running kalign benchmarks and viewing results. - -Usage: - python -m benchmarks.app [--port 8050] -""" - -import argparse -import json -import sys -import threading -from pathlib import Path - -try: - import dash - from dash import dcc, html, dash_table, callback_context - from dash.dependencies import Input, Output, State - import plotly.express as px - import pandas as pd -except ImportError: - print("Dash visualization requires extra dependencies:") - print(" pip install dash plotly pandas") - sys.exit(1) - -from .datasets import DATASETS, get_cases, download_dataset -from .scoring import run_case - -RESULTS_DIR = Path(__file__).parent / "results" -RESULTS_DIR.mkdir(parents=True, exist_ok=True) - -# --------------------------------------------------------------------------- -# Configuration presets — the 4 varieties of kalign -# --------------------------------------------------------------------------- - -CONFIG_PRESETS = { - "Kalign (default)": {"refine": "none", "ensemble": 0}, - "Kalign + Refinement": {"refine": "confident", "ensemble": 0}, - "Ensemble (8 runs)": {"refine": "none", "ensemble": 8}, - "Ensemble (12 runs)": {"refine": "none", "ensemble": 12}, - "Clustal Omega": {"method": "clustalo", "refine": "none", "ensemble": 0}, - "MAFFT": {"method": "mafft", "refine": "none", "ensemble": 0}, - "MUSCLE": {"method": "muscle", "refine": "none", "ensemble": 0}, -} - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -def _available_datasets(): - """Return list of (name, available, n_cases) tuples.""" - info = [] - for name, ds in DATASETS.items(): - avail = ds["is_available"]() - n = len(ds["cases"]()) if avail else 0 - info.append((name, avail, n)) - return info - - -def _load_json(path): - with open(path) as f: - return json.load(f) - - -_EXTERNAL_LABELS = { - "clustalo": "Clustal Omega", - "mafft": "MAFFT", - "muscle": "MUSCLE", -} - - -def _config_label(row): - """Build a human-readable configuration label from result fields.""" - method = row.get("method", "python_api") - if method in _EXTERNAL_LABELS: - return _EXTERNAL_LABELS[method] - - ensemble = row.get("ensemble", 0) - refine = row.get("refine", "none") - - if ensemble and ensemble > 0: - label = f"Ensemble ({ensemble})" - elif refine and refine != "none": - label = f"Kalign + {refine}" - else: - label = "Kalign" - return label - - -def _results_to_df(results): - """Convert list of AlignmentResult dicts to DataFrame.""" - df = pd.DataFrame(results) - if "error" in df.columns: - df = df[df["error"].isna() | (df["error"] == "None") | (df["error"].isnull())] - # Ensure columns exist (backward compat with old JSON files) - if "ensemble" not in df.columns: - df["ensemble"] = 0 - df["ensemble"] = df["ensemble"].fillna(0).astype(int) - for col in ("recall", "precision", "f1", "tc"): - if col not in df.columns: - df[col] = 0.0 - df[col] = df[col].fillna(0.0) - # Add config label for plotting - df["config"] = df.apply(_config_label, axis=1) - return df - - -def _build_figures(df): - """Build plotly figures from a results DataFrame.""" - figs = [] - if df.empty: - return figs - - # Filter out errors - clean = df[df["sp_score"] > 0].copy() if "sp_score" in df.columns else df - - if clean.empty: - return figs - - # Ensure new columns exist (backward compat with old JSON files) - for col in ("recall", "precision", "f1", "tc"): - if col not in clean.columns: - clean[col] = 0.0 - - # Determine color column: use 'config' if multiple configs, else 'method' - configs = clean["config"].nunique() - color_col = "config" if configs > 1 else "method" - - # bali_score-compatible metrics - has_recall = clean["recall"].sum() > 0 - if has_recall: - # SP score (bali_score compatible) = recall - fig_sp = px.box( - clean, x="dataset", y="recall", color=color_col, - title="SP Score by Dataset (bali_score compatible)", - labels={"recall": "SP Score", "dataset": "Dataset", "config": "Configuration"}, - ) - fig_sp.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("sp_box", fig_sp)) - - # TC score - fig_tc = px.box( - clean, x="dataset", y="tc", color=color_col, - title="TC Score by Dataset", - labels={"tc": "TC Score", "dataset": "Dataset", "config": "Configuration"}, - ) - fig_tc.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("tc_box", fig_tc)) - - # Precision - fig_prec = px.box( - clean, x="dataset", y="precision", color=color_col, - title="Precision by Dataset", - labels={"precision": "Precision", "dataset": "Dataset", "config": "Configuration"}, - ) - fig_prec.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("precision_box", fig_prec)) - - # F1 - fig_f1 = px.box( - clean, x="dataset", y="f1", color=color_col, - title="F1 Score by Dataset", - labels={"f1": "F1", "dataset": "Dataset", "config": "Configuration"}, - ) - fig_f1.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("f1_box", fig_f1)) - else: - # Fallback: legacy SP score (not bali_score compatible) - fig_box = px.box( - clean, x="dataset", y="sp_score", color=color_col, - title="SP Score Distribution by Dataset (legacy)", - labels={"sp_score": "SP Score", "dataset": "Dataset", "config": "Configuration"}, - ) - fig_box.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("sp_box", fig_box)) - - # Strip plot by family - hover_cols = ["family", "wall_time", "refine", "ensemble"] - if has_recall: - hover_cols.extend(["recall", "precision", "f1", "tc"]) - y_strip = "recall" if has_recall else "sp_score" - fig_scatter = px.strip( - clean, x="dataset", y=y_strip, color=color_col, - hover_data=hover_cols, - title="SP Score per Family", - labels={"recall": "SP Score", "sp_score": "SP Score (legacy)", "config": "Configuration"}, - ) - fig_scatter.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("scatter", fig_scatter)) - - # Timing - fig_time = px.box( - clean, x="dataset", y="wall_time", color=color_col, - title="Alignment Time by Dataset", - labels={"wall_time": "Wall Time (s)", "dataset": "Dataset", "config": "Configuration"}, - ) - fig_time.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("timing", fig_time)) - - # Summary table by config x dataset - if configs > 1: - agg_dict = { - "mean_sp": ("sp_score", "mean"), - "median_sp": ("sp_score", "median"), - "mean_time": ("wall_time", "mean"), - "n_cases": ("sp_score", "count"), - } - if has_recall: - agg_dict.update({ - "mean_recall": ("recall", "mean"), - "mean_precision": ("precision", "mean"), - "mean_f1": ("f1", "mean"), - "mean_tc": ("tc", "mean"), - }) - summary = clean.groupby(["config", "dataset"]).agg(**agg_dict).reset_index() - summary = summary.round(3) - y_col = "mean_recall" if has_recall else "mean_sp" - y_label = "Mean SP Score" if has_recall else "Mean SP Score (legacy)" - fig_summary = px.bar( - summary, x="dataset", y=y_col, color="config", - barmode="group", - title=f"{y_label} by Dataset and Configuration", - labels={y_col: y_label, "dataset": "Dataset", "config": "Configuration"}, - ) - fig_summary.update_layout(legend=dict(orientation="h", y=-0.2)) - figs.append(("summary_bar", fig_summary)) - - return figs - - -# --------------------------------------------------------------------------- -# State shared between callbacks and the benchmark thread -# --------------------------------------------------------------------------- - -_run_state = { - "running": False, - "progress": "", - "results": [], - "done": False, -} - - -def _run_in_thread(dataset, method, binary, max_cases, n_threads, configs): - """Run benchmarks in a background thread so the UI stays responsive.""" - global _run_state - _run_state["running"] = True - _run_state["done"] = False - _run_state["results"] = [] - _run_state["progress"] = f"Loading {dataset} cases..." - - try: - cases = get_cases(dataset, max_cases=max_cases if max_cases > 0 else None) - if not cases: - _run_state["progress"] = f"No cases found for {dataset}. Try downloading first." - _run_state["running"] = False - _run_state["done"] = True - return - - total = len(cases) * len(configs) - done = 0 - - for cfg_name, cfg in configs: - cfg_method = cfg.get("method", method) - refine = cfg["refine"] - ensemble = cfg["ensemble"] - - for case in cases: - done += 1 - _run_state["progress"] = ( - f"[{done}/{total}] {cfg_name}: {case.family}..." - ) - result = run_case( - case, method=cfg_method, binary=binary, - n_threads=n_threads, refine=refine, ensemble=ensemble, - ) - _run_state["results"].append(result.to_dict()) - - # Auto-save - import time as _time - save_path = RESULTS_DIR / f"run_{_time.strftime('%Y%m%d_%H%M%S')}.json" - data = { - "timestamp": _time.strftime("%Y-%m-%dT%H:%M:%S"), - "results": _run_state["results"], - } - with open(save_path, "w") as f: - json.dump(data, f, indent=2) - - _run_state["progress"] = f"Done! {done} alignments scored. Saved to {save_path.name}" - except Exception as e: - _run_state["progress"] = f"Error: {e}" - finally: - _run_state["running"] = False - _run_state["done"] = True - - -# --------------------------------------------------------------------------- -# App layout -# --------------------------------------------------------------------------- - -def create_app(): - app = dash.Dash(__name__) - app.title = "Kalign Benchmarks" - - # Discover saved result files - def _saved_files(): - return sorted(RESULTS_DIR.glob("*.json"), reverse=True) - - ds_info = _available_datasets() - - app.layout = html.Div([ - html.H1("Kalign Benchmark Dashboard"), - - # --- Run controls --- - html.Div([ - html.H3("Run Benchmark"), - html.Div([ - # Row 1: Dataset, method, binary - html.Div([ - html.Div([ - html.Label("Dataset"), - dcc.Dropdown( - id="dataset-dropdown", - options=[ - {"label": f"{name} ({'available' if avail else 'needs download'}, {n} cases)", - "value": name} - for name, avail, n in ds_info - ], - value="balibase", - ), - ], style={"width": "30%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Label("Method"), - dcc.RadioItems( - id="method-radio", - options=[ - {"label": " Python API", "value": "python_api"}, - {"label": " C binary", "value": "cli"}, - ], - value="python_api", - ), - ], style={"width": "12%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Label("C binary path"), - dcc.Input(id="binary-input", type="text", value="build/src/kalign", - style={"width": "160px"}), - ], style={"width": "18%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Label("Max cases (0=all)"), - dcc.Input(id="max-cases-input", type="number", value=0, min=0, style={"width": "80px"}), - ], style={"width": "10%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Label("Threads"), - dcc.Input(id="threads-input", type="number", value=1, min=1, style={"width": "60px"}), - ], style={"width": "8%", "display": "inline-block", "verticalAlign": "top"}), - ]), - - html.Hr(style={"margin": "10px 0"}), - - # Row 2: Configuration presets - html.Div([ - html.Div([ - html.Label("Configurations to run"), - dcc.Checklist( - id="config-checklist", - options=[{"label": f" {name}", "value": name} - for name in CONFIG_PRESETS], - value=["Kalign (default)"], - style={"lineHeight": "2"}, - ), - ], style={"width": "35%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Label("Custom ensemble runs"), - dcc.Input(id="custom-ensemble-input", type="number", value=8, min=2, max=32, - style={"width": "80px"}), - html.Br(), - html.Label("Custom refine mode", style={"marginTop": "8px"}), - dcc.Dropdown( - id="custom-refine-dropdown", - options=[ - {"label": "none", "value": "none"}, - {"label": "confident", "value": "confident"}, - {"label": "all", "value": "all"}, - ], - value="none", - style={"width": "140px"}, - ), - html.Button("+ Add custom config", id="add-custom-btn", n_clicks=0, - style={"marginTop": "8px"}), - ], style={"width": "25%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Br(), - html.Button("Download Dataset", id="download-btn", n_clicks=0, - style={"marginRight": "10px"}), - html.Br(), html.Br(), - html.Button("Run Benchmark", id="run-btn", n_clicks=0, - style={"backgroundColor": "#4CAF50", "color": "white", - "border": "none", "padding": "12px 24px", "cursor": "pointer", - "fontSize": "14px"}), - ], style={"width": "20%", "display": "inline-block", "verticalAlign": "top"}), - ]), - ]), - html.Div(id="progress-text", style={"marginTop": "10px", "fontStyle": "italic"}), - dcc.Interval(id="progress-interval", interval=1000, n_intervals=0, disabled=True), - # Hidden store for custom configs - dcc.Store(id="custom-configs-store", data=[]), - ], style={"padding": "15px", "backgroundColor": "#f5f5f5", "borderRadius": "8px", "marginBottom": "20px"}), - - # --- Load saved results --- - html.Div([ - html.H3("View Results"), - html.Div([ - html.Div([ - html.Label("Saved result files"), - dcc.Dropdown(id="results-dropdown", multi=True), - ], style={"width": "60%", "display": "inline-block", "verticalAlign": "top", "marginRight": "2%"}), - html.Div([ - html.Br(), - html.Button("Refresh file list", id="refresh-btn", n_clicks=0, - style={"marginRight": "10px"}), - html.Button("Load & Plot", id="load-btn", n_clicks=0), - ], style={"width": "30%", "display": "inline-block", "verticalAlign": "top"}), - ]), - ], style={"padding": "15px", "backgroundColor": "#f0f8ff", "borderRadius": "8px", "marginBottom": "20px"}), - - # --- Charts --- - html.Div(id="charts-container"), - - # --- Table --- - html.Div(id="table-container"), - - ], style={"maxWidth": "1200px", "margin": "0 auto", "padding": "20px", "fontFamily": "sans-serif"}) - - # --- Callbacks --- - - @app.callback( - Output("results-dropdown", "options"), - Input("refresh-btn", "n_clicks"), - Input("progress-interval", "n_intervals"), - ) - def refresh_file_list(_n1, _n2): - files = _saved_files() - return [{"label": f.name, "value": str(f)} for f in files] - - @app.callback( - Output("custom-configs-store", "data"), - Output("config-checklist", "options"), - Output("config-checklist", "value"), - Input("add-custom-btn", "n_clicks"), - State("custom-ensemble-input", "value"), - State("custom-refine-dropdown", "value"), - State("custom-configs-store", "data"), - State("config-checklist", "options"), - State("config-checklist", "value"), - prevent_initial_call=True, - ) - def add_custom_config(_n, ensemble_n, refine, custom_cfgs, options, selected): - """Add a custom configuration to the checklist.""" - ensemble_n = ensemble_n or 0 - refine = refine or "none" - - if ensemble_n > 0: - name = f"Ensemble ({ensemble_n}) + {refine}" - else: - name = f"Custom (refine={refine})" - - cfg = {"refine": refine, "ensemble": ensemble_n} - - # Avoid duplicates - for existing in custom_cfgs: - if existing["name"] == name: - return custom_cfgs, options, selected - - custom_cfgs.append({"name": name, **cfg}) - options.append({"label": f" {name}", "value": name}) - selected.append(name) - return custom_cfgs, options, selected - - @app.callback( - Output("progress-text", "children"), - Output("progress-interval", "disabled"), - Input("run-btn", "n_clicks"), - Input("download-btn", "n_clicks"), - Input("progress-interval", "n_intervals"), - State("dataset-dropdown", "value"), - State("method-radio", "value"), - State("binary-input", "value"), - State("max-cases-input", "value"), - State("threads-input", "value"), - State("config-checklist", "value"), - State("custom-configs-store", "data"), - prevent_initial_call=True, - ) - def handle_run_or_download(run_clicks, dl_clicks, _n_intervals, - dataset, method, binary, max_cases, n_threads, - selected_configs, custom_cfgs): - triggered = callback_context.triggered_id - - if triggered == "download-btn" and not _run_state["running"]: - try: - download_dataset(dataset) - return f"Downloaded {dataset}.", True - except Exception as e: - return f"Download error: {e}", True - - if triggered == "run-btn" and not _run_state["running"]: - if not selected_configs: - return "Select at least one configuration.", True - - # Build config list from presets + custom - configs = [] - custom_by_name = {c["name"]: c for c in (custom_cfgs or [])} - for name in selected_configs: - if name in CONFIG_PRESETS: - configs.append((name, CONFIG_PRESETS[name])) - elif name in custom_by_name: - c = custom_by_name[name] - configs.append((name, {"refine": c["refine"], "ensemble": c["ensemble"]})) - - if not configs: - return "No valid configurations selected.", True - - t = threading.Thread( - target=_run_in_thread, - args=(dataset, method, binary or "build/src/kalign", - max_cases or 0, n_threads or 1, configs), - daemon=True, - ) - t.start() - return f"Starting {len(configs)} configuration(s)...", False - - # Polling progress - if _run_state["running"]: - return _run_state["progress"], False - if _run_state["done"]: - return _run_state["progress"], True - - return dash.no_update, dash.no_update - - @app.callback( - Output("charts-container", "children"), - Output("table-container", "children"), - Input("load-btn", "n_clicks"), - Input("progress-interval", "n_intervals"), - State("results-dropdown", "value"), - prevent_initial_call=True, - ) - def update_charts(load_clicks, _n_intervals, selected_files): - triggered = callback_context.triggered_id - - # If benchmark just finished, show those results immediately - if triggered == "progress-interval" and _run_state["done"] and _run_state["results"]: - df = _results_to_df(_run_state["results"]) - elif triggered == "load-btn" and selected_files: - all_results = [] - for path in selected_files: - data = _load_json(path) - for r in data["results"]: - r["source"] = Path(path).stem - all_results.extend(data["results"]) - df = _results_to_df(all_results) - else: - return dash.no_update, dash.no_update - - if df.empty: - return html.P("No results to display."), "" - - charts = [] - for fig_id, fig in _build_figures(df): - charts.append(dcc.Graph(id=fig_id, figure=fig)) - - # Table - display_cols = [c for c in df.columns if c not in ("error",)] - table = dash_table.DataTable( - data=df.to_dict("records"), - columns=[{"name": c, "id": c} for c in display_cols], - filter_action="native", - sort_action="native", - page_size=50, - style_table={"overflowX": "auto"}, - style_cell={"textAlign": "left", "padding": "5px"}, - style_header={"fontWeight": "bold"}, - ) - - return charts, html.Div([html.H3("Results Table"), table]) - - return app - - -def main(): - parser = argparse.ArgumentParser( - description="Kalign benchmark dashboard", - prog="python -m benchmarks.app", - ) - parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)") - parser.add_argument("--port", type=int, default=8050, help="Port (default: 8050)") - args = parser.parse_args() - - app = create_app() - print(f"Starting dashboard at http://{args.host}:{args.port}") - app.run(debug=False, host=args.host, port=args.port) - - -if __name__ == "__main__": - main() diff --git a/benchmarks/bench_quality_timing.py b/benchmarks/bench_quality_timing.py deleted file mode 100644 index 83b00ce..0000000 --- a/benchmarks/bench_quality_timing.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Benchmark kalign mode presets (fast/default/recall/accurate) vs external tools. - -Compares all 4 kalign NSGA-III optimized mode presets against ClustalO, -MAFFT, and MUSCLE. Reports quality metrics and wall time. -Each timing measurement is repeated N times and the median is reported. - -Usage (inside container): - python -m benchmarks.bench_modes --threads 16 - python -m benchmarks.bench_modes --threads 8 --runs 3 --output results.json -""" - -import argparse -import json -import statistics -import time -from pathlib import Path - -from .datasets import get_cases, download_dataset -from .scoring import run_case - - -KALIGN_MODES = ["fast", "default", "recall", "accurate"] - - -def main(): - parser = argparse.ArgumentParser( - description="Benchmark kalign modes vs external tools", - prog="python -m benchmarks.bench_modes", - ) - parser.add_argument( - "--threads", type=int, default=1, - help="Threads for all tools (default: 1)", - ) - parser.add_argument( - "--runs", type=int, default=3, - help="Timing repeats per method/case; report median (default: 3)", - ) - parser.add_argument( - "--output", type=str, default="", - help="Save results to JSON file", - ) - parser.add_argument( - "--dataset", default="balibase", - help="Dataset to benchmark (default: balibase)", - ) - args = parser.parse_args() - - download_dataset(args.dataset) - cases = get_cases(args.dataset) - if not cases: - print(f"No cases found for '{args.dataset}'.") - return - - print(f"Dataset: {args.dataset} ({len(cases)} cases)") - print(f"Threads: {args.threads}, Timing repeats: {args.runs}") - print() - - # Build method list: 4 kalign modes + 3 external tools - methods = [] - for mode in KALIGN_MODES: - methods.append((f"kalign {mode}", dict(method="python_api", mode=mode))) - for tool in ["clustalo", "mafft", "muscle"]: - methods.append((tool, dict(method=tool))) - - all_results = {} - for label, kwargs in methods: - print(f"Running {label}...", flush=True) - - # First run: collect quality metrics - quality_results = [] - timing_runs = {c.family: [] for c in cases} - t0 = time.perf_counter() - - for i, case in enumerate(cases): - r = run_case(case, n_threads=args.threads, **kwargs) - quality_results.append(r) - timing_runs[case.family].append(r.wall_time) - if r.error: - print(f" [{i+1}/{len(cases)}] {r.family}: ERROR {r.error}") - - # Additional timing repeats - for rep in range(1, args.runs): - for case in cases: - r = run_case(case, n_threads=args.threads, **kwargs) - if not r.error: - timing_runs[case.family].append(r.wall_time) - - elapsed = time.perf_counter() - t0 - ok = [r for r in quality_results if not r.error] - - if ok: - # Median wall time per case, then sum - median_times = [] - for r in ok: - times = timing_runs[r.family] - median_times.append(statistics.median(times) if times else r.wall_time) - total_median_time = sum(median_times) - - rec = statistics.mean([r.recall for r in ok]) - pre = statistics.mean([r.precision for r in ok]) - f1 = statistics.mean([r.f1 for r in ok]) - tc = statistics.mean([r.tc for r in ok]) - - all_results[label] = { - "n_cases": len(ok), - "recall": round(rec, 4), - "precision": round(pre, 4), - "f1": round(f1, 4), - "tc": round(tc, 4), - "median_total_time": round(total_median_time, 2), - "elapsed": round(elapsed, 1), - } - print(f" {len(ok)} cases, median total {total_median_time:.1f}s " - f"(elapsed {elapsed:.1f}s)") - else: - all_results[label] = {"n_cases": 0, "error": "all failed"} - print(f" All cases failed") - print() - - # Summary table - print(f"{'Method':<20} {'N':>4} {'Recall':>8} {'Prec':>8} {'F1':>8} " - f"{'TC':>8} {'Time(s)':>10}") - print("-" * 72) - for label, _ in methods: - s = all_results.get(label, {}) - if s.get("n_cases", 0) == 0: - print(f"{label:<20} {'0':>4} {'n/a':>8} {'n/a':>8} {'n/a':>8} " - f"{'n/a':>8} {'n/a':>10}") - continue - print(f"{label:<20} {s['n_cases']:>4} {s['recall']:>8.3f} {s['precision']:>8.3f} " - f"{s['f1']:>8.3f} {s['tc']:>8.3f} {s['median_total_time']:>10.1f}") - - if args.output: - out = Path(args.output) - out.parent.mkdir(parents=True, exist_ok=True) - with open(out, "w") as f: - json.dump(all_results, f, indent=2) - print(f"\nResults saved to {out}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/combined_improvements.py b/benchmarks/combined_improvements.py deleted file mode 100644 index 349f3ec..0000000 --- a/benchmarks/combined_improvements.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Measure cumulative/additive effect of kalign improvements on BAliBASE. - -Tests individual features and their combinations to determine additivity: - Individual features (from baseline): - 1. baseline: vsm_amax=0, no extras - 2. +vsm: vsm_amax=2.0 only - 3. +ref: refine=confident only (vsm=0) - 4. +re1: realign=1 only (vsm=0) - Stacking (additive): - 5. +vsm+ref: vsm + refine - 6. +vsm+re1: vsm + realign=1 (no refine) - 7. +vsm+ref+re1: vsm + refine + realign=1 - Ensemble path: - 8. +ens3: ensemble=3 (uses vsm, internal post-refine) - 9. +ens5: ensemble=5 - 10. +ens5+adapt: ensemble=5 + adaptive_budget - -Usage: - uv run python -m benchmarks.combined_improvements -j 4 - uv run python -m benchmarks.combined_improvements -j 4 --categories RV11 RV12 -""" - -import argparse -import json -import statistics -import tempfile -import time -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path - -import kalign -from .datasets import get_cases -from .scoring import parse_balibase_xml - - -CONFIGS = [ - # --- Individual features (from baseline) --- - { - "name": "baseline", - "desc": "old defaults (vsm=0, no extras)", - "vsm_amax": 0.0, - "dist_scale": 0.0, - "refine": "none", - "ensemble": 0, - "adaptive_budget": False, - "realign": 0, - }, - { - "name": "+vsm", - "desc": "vsm_amax=2.0 only", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "none", - "ensemble": 0, - "adaptive_budget": False, - "realign": 0, - }, - { - "name": "+ref", - "desc": "refine=confident only (vsm=0)", - "vsm_amax": 0.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 0, - "adaptive_budget": False, - "realign": 0, - }, - { - "name": "+re1", - "desc": "realign=1 only (vsm=0)", - "vsm_amax": 0.0, - "dist_scale": 0.0, - "refine": "none", - "ensemble": 0, - "adaptive_budget": False, - "realign": 1, - }, - # --- Stacking (additive combinations) --- - { - "name": "+vsm+ref", - "desc": "vsm + refine", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 0, - "adaptive_budget": False, - "realign": 0, - }, - { - "name": "+vsm+re1", - "desc": "vsm + realign=1 (no refine)", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "none", - "ensemble": 0, - "adaptive_budget": False, - "realign": 1, - }, - { - "name": "+vsm+ref+re1", - "desc": "vsm + refine + realign=1 (full stack)", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 0, - "adaptive_budget": False, - "realign": 1, - }, - # --- Ensemble path --- - { - "name": "+ens3", - "desc": "ensemble=3 (uses vsm, internal post-refine)", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 3, - "adaptive_budget": False, - "realign": 0, - }, - { - "name": "+ens5", - "desc": "ensemble=5 (uses vsm, internal post-refine)", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 5, - "adaptive_budget": False, - "realign": 0, - }, - { - "name": "+ens5+adapt", - "desc": "ensemble=5 + adaptive_budget", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 5, - "adaptive_budget": True, - "realign": 0, - }, - # --- Ensemble + post-realign --- - { - "name": "+ens3+re1", - "desc": "ensemble=3 + post-ensemble realign=1", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 3, - "adaptive_budget": False, - "realign": 1, - }, - { - "name": "+ens5+re1", - "desc": "ensemble=5 + post-ensemble realign=1", - "vsm_amax": 2.0, - "dist_scale": 0.0, - "refine": "confident", - "ensemble": 5, - "adaptive_budget": False, - "realign": 1, - }, -] - - -def _score_case(case, output_path): - xml_path = case.reference.with_suffix(".xml") - if xml_path.exists(): - mask = parse_balibase_xml(xml_path) - return kalign.compare_detailed(str(case.reference), str(output_path), column_mask=mask) - return kalign.compare_detailed(str(case.reference), str(output_path)) - - -def _run_one(args): - case, config = args - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - t0 = time.perf_counter() - kalign.align_file_to_file( - str(case.unaligned), tmp_path, format="fasta", - seq_type=case.seq_type, - vsm_amax=config["vsm_amax"], - dist_scale=config["dist_scale"], - refine=config["refine"], - ensemble=config["ensemble"], - adaptive_budget=config["adaptive_budget"], - realign=config.get("realign", 0), - ) - wall_time = time.perf_counter() - t0 - sp_score = kalign.compare(str(case.reference), tmp_path) - scores = _score_case(case, tmp_path) - return { - "family": case.family, "dataset": case.dataset, - "config": config["name"], - "sp_score": sp_score, - "recall": scores["recall"], "precision": scores["precision"], - "f1": scores["f1"], "tc": scores["tc"], - "wall_time": wall_time, - } - except Exception as e: - return { - "family": case.family, "dataset": case.dataset, - "config": config["name"], - "sp_score": 0, "recall": 0, "precision": 0, "f1": 0, "tc": 0, - "wall_time": 0, "error": str(e), - } - finally: - Path(tmp_path).unlink(missing_ok=True) - - -def main(): - parser = argparse.ArgumentParser(description="Combined improvements sweep on BAliBASE") - parser.add_argument("-j", "--parallel", type=int, default=4) - parser.add_argument("--max-cases", type=int, default=0) - parser.add_argument("--categories", nargs="*", default=None, - help="BAliBASE categories (e.g. RV11 RV12)") - args = parser.parse_args() - - cases = get_cases("balibase", max_cases=args.max_cases if args.max_cases else None) - if args.categories: - cats = [c.upper() for c in args.categories] - cases = [c for c in cases if any(cat in c.dataset.upper() for cat in cats)] - - print(f"{len(cases)} BAliBASE cases x {len(CONFIGS)} configs = {len(cases) * len(CONFIGS)} tasks") - - tasks = [(case, config) for case in cases for config in CONFIGS] - - t0 = time.perf_counter() - results = [] - done = 0 - with ProcessPoolExecutor(max_workers=args.parallel) as pool: - futures = {pool.submit(_run_one, t): t for t in tasks} - for f in as_completed(futures): - done += 1 - r = f.result() - results.append(r) - if done % 100 == 0: - print(f" {done}/{len(tasks)} ({time.perf_counter()-t0:.0f}s)") - - elapsed = time.perf_counter() - t0 - print(f"All done in {elapsed:.0f}s\n") - - # Overall summary - print("=" * 90) - print("OVERALL") - print("=" * 90) - _print_config_table(results) - - # Per-category - all_cats = sorted({r["dataset"] for r in results}) - for cat in all_cats: - cat_results = [r for r in results if r["dataset"] == cat] - n_cases = len(cat_results) // len(CONFIGS) - cat_label = cat.replace("balibase_", "") - print(f"\n{'=' * 90}") - print(f"{cat_label} ({n_cases} cases)") - print(f"{'=' * 90}") - _print_config_table(cat_results) - - # Save - out = Path("benchmarks/results/combined_improvements.json") - out.parent.mkdir(parents=True, exist_ok=True) - with open(out, "w") as f: - json.dump({"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), "results": results}, f, indent=2) - print(f"\nSaved to {out}") - - -def _print_config_table(results): - config_names = [c["name"] for c in CONFIGS] - groups = defaultdict(list) - for r in results: - if "error" not in r: - groups[r["config"]].append(r) - - print(f"{'Config':<16} {'SP':>8} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time(s)':>9} {'dSP':>6} {'dF1':>6}") - print("-" * 92) - baseline_sp = None - baseline_f1 = None - for name in config_names: - entries = groups.get(name, []) - if not entries: - continue - sp = statistics.mean(r["sp_score"] for r in entries) - rec = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - total_time = sum(r["wall_time"] for r in entries) - if baseline_f1 is None: - baseline_sp = sp - baseline_f1 = f1 - dsp = "" - df1 = "" - else: - dsp = f"{sp - baseline_sp:+.1f}" - df1 = f"{f1 - baseline_f1:+.3f}" - print(f"{name:<16} {sp:>8.1f} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f} {total_time:>9.1f} {dsp:>6} {df1:>6}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/eval_checkpoint_configs.py b/benchmarks/eval_checkpoint_configs.py deleted file mode 100644 index 49b67e4..0000000 --- a/benchmarks/eval_checkpoint_configs.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -"""Evaluate selected configs from an NSGA-III checkpoint on full BAliBASE. - -Reports true SP (recall), precision, F1, TC, and wall time — not CV estimates. -""" - -import pickle -import sys -import time -from pathlib import Path - - -# Ensure the benchmarks package is importable -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from benchmarks.optimize_unified import ( - decode_unified_params, - evaluate_unified, - set_active_profile, -) -from benchmarks.datasets import balibase_cases, balibase_download, balibase_is_available - - -def main(): - checkpoint_path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/gen_checkpoint.pkl" - - with open(checkpoint_path, "rb") as f: - ckpt = pickle.load(f) - - X = ckpt["pop_X"] - F = ckpt["pop_F"] - max_runs = ckpt["max_runs"] - f1_cv = -F[:, 0] - tc_cv = -F[:, 1] - - # Selected configs to evaluate - selected = { - 122: "Hyper-fast bare (n=1,no refine)", - 67: "Fast+refine (n=1,ref=CONF)", - } - - # Set up BAliBASE - profile = ckpt.get("profile", "protein") - set_active_profile(profile) - - if not balibase_is_available(): - print("Downloading BAliBASE...", flush=True) - balibase_download() - - cases = balibase_cases() - print(f"Loaded {len(cases)} BAliBASE cases (profile: {profile})\n") - - # Print header - print(f"{'Idx':>4} {'Label':<25} {'CV_F1':>6} {'CV_TC':>6} " - f"{'Recall':>7} {'Prec':>7} {'F1':>7} {'TC':>7} {'Time':>8}") - print("-" * 95) - - results = {} - for idx, label in selected.items(): - x = X[idx] - params = decode_unified_params(x, max_runs) - - print(f"[{idx:3d}] {label:<25} {f1_cv[idx]:.4f} {tc_cv[idx]:.4f} ", - end="", flush=True) - - start = time.perf_counter() - result = evaluate_unified(params, cases, n_threads=1, quiet=False) - elapsed = time.perf_counter() - start - - print(f"{result['recall']:.4f} {result['precision']:.4f} " - f"{result['f1']:.4f} {result['tc']:.4f} {elapsed:7.1f}s") - - results[idx] = { - "label": label, - "cv_f1": f1_cv[idx], - "cv_tc": tc_cv[idx], - "params": params, - **result, - } - - # Summary table - print("\n" + "=" * 95) - print("FULL BENCHMARK RESULTS vs COMPETITORS") - print("=" * 95) - print(f"\n{'Method':<30} {'Recall(SP)':>10} {'Prec':>7} {'F1':>7} {'TC':>7} {'Time':>8}") - print("-" * 75) - - for idx, label in selected.items(): - r = results[idx] - print(f"kalign [{idx}] {label:<20} {r['recall']:>10.4f} {r['precision']:>7.4f} " - f"{r['f1']:>7.4f} {r['tc']:>7.4f} {r['wall_time']:>7.1f}s") - - print("-" * 75) - print(f"{'kalign fast (shipped)':<30} {'0.809':>10} {'0.663':>7} {'0.723':>7} {'0.482':>7} {'10':>7}s") - print(f"{'kalign default (shipped)':<30} {'0.816':>10} {'0.758':>7} {'0.780':>7} {'0.490':>7} {'101':>7}s") - print(f"{'kalign accurate (shipped)':<30} {'0.837':>10} {'0.719':>7} {'0.769':>7} {'0.518':>7} {'262':>7}s") - print(f"{'ClustalO':<30} {'0.840':>10} {'0.710':>7} {'0.764':>7} {'0.559':>7}") - print(f"{'MAFFT':<30} {'0.867':>10} {'0.715':>7} {'0.778':>7} {'0.590':>7}") - print(f"{'MUSCLE':<30} {'0.870':>10} {'0.721':>7} {'0.783':>7} {'0.581':>7}") - - # Per-category breakdown for each config - for idx, label in selected.items(): - r = results[idx] - if "per_category" in r and r["per_category"]: - print(f"\n--- [{idx}] {label} per-category ---") - for cat in sorted(r["per_category"]): - c = r["per_category"][cat] - print(f" {cat:<20} Recall={c['recall']:.4f} Prec={c['precision']:.4f} " - f"F1={c['f1']:.4f} TC={c['tc']:.4f} (n={c['n']})") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/external_balibase.py b/benchmarks/external_balibase.py deleted file mode 100644 index d2f7109..0000000 --- a/benchmarks/external_balibase.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Run external tools (mafft, muscle, clustalo) on BAliBASE inside container. - -Scores against references using kalign's compare_detailed with XML masks. - -Usage (inside container): - python -m benchmarks.external_balibase -""" - -import json -import shutil -import statistics -import subprocess -import tempfile -import time -from collections import defaultdict -from pathlib import Path - -import kalign -from .datasets import get_cases -from .scoring import parse_balibase_xml - - -def _run_external(tool, unaligned, output): - """Run an external alignment tool.""" - try: - if tool == "mafft": - with open(output, "w") as f: - subprocess.run( - ["mafft", "--auto", str(unaligned)], - stdout=f, stderr=subprocess.PIPE, check=True, - ) - elif tool == "clustalo": - subprocess.run( - ["clustalo", "-i", str(unaligned), "-o", str(output), - "--outfmt=fasta", "--force"], - capture_output=True, check=True, - ) - elif tool == "muscle": - subprocess.run( - ["muscle", "-align", str(unaligned), "-output", str(output)], - capture_output=True, check=True, - ) - return True - except (subprocess.CalledProcessError, FileNotFoundError): - return False - - -def _score_case(case, output_path): - xml_path = case.reference.with_suffix(".xml") - if xml_path.exists(): - mask = parse_balibase_xml(xml_path) - return kalign.compare_detailed(str(case.reference), str(output_path), column_mask=mask) - return kalign.compare_detailed(str(case.reference), str(output_path)) - - -def main(): - tools = [] - for t in ["mafft", "muscle", "clustalo"]: - if shutil.which(t): - tools.append(t) - else: - print(f" {t}: not found, skipping") - - if not tools: - print("No external tools found. Run inside container.") - return - - print(f"Tools: {tools}") - - cases = get_cases("balibase") - print(f"{len(cases)} BAliBASE cases") - - results = [] - n_tasks = len(tools) * len(cases) - done = 0 - t0 = time.perf_counter() - - for tool in tools: - print(f"\n Tool: {tool}", flush=True) - for case in cases: - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - t1 = time.perf_counter() - ok = _run_external(tool, case.unaligned, tmp_path) - wall = time.perf_counter() - t1 - if ok: - scores = _score_case(case, tmp_path) - results.append({ - "family": case.family, "dataset": case.dataset, - "method": tool, - "recall": scores["recall"], "precision": scores["precision"], - "f1": scores["f1"], "tc": scores["tc"], - "wall_time": wall, - }) - else: - results.append({ - "family": case.family, "dataset": case.dataset, - "method": tool, - "recall": 0, "precision": 0, "f1": 0, "tc": 0, - "wall_time": 0, "error": f"{tool} failed", - }) - finally: - Path(tmp_path).unlink(missing_ok=True) - - done += 1 - if done % 20 == 0: - elapsed = time.perf_counter() - t0 - eta = elapsed / done * (n_tasks - done) - print(f" {done}/{n_tasks} ({elapsed:.0f}s, ETA {eta:.0f}s)", flush=True) - - elapsed = time.perf_counter() - t0 - print(f"\nAll done in {elapsed:.0f}s") - - # Summary - groups = defaultdict(list) - for r in results: - groups[r["method"]].append(r) - - print(f"\n{'Method':>12} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time':>7}") - print("-" * 60) - for tool in tools: - entries = [r for r in groups[tool] if "error" not in r] - if not entries: - print(f"{tool:>12} (no results)") - continue - rec = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - wt = sum(r["wall_time"] for r in entries) - print(f"{tool:>12} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f} {wt:>6.1f}s") - - # Per-category - all_cats = sorted({r["dataset"].replace("balibase_", "") for r in results}) - for cat in all_cats: - cat_results = [r for r in results if cat in r["dataset"]] - cat_groups = defaultdict(list) - for r in cat_results: - cat_groups[r["method"]].append(r) - n = len(cat_groups.get(tools[0], [])) - print(f"\n=== {cat} ({n} cases) ===") - print(f"{'Method':>12} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8}") - print("-" * 50) - for tool in tools: - entries = [r for r in cat_groups.get(tool, []) if "error" not in r] - if not entries: - continue - rec = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - print(f"{tool:>12} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f}") - - out = Path("benchmarks/data/external_balibase.json") - out.parent.mkdir(parents=True, exist_ok=True) - json.dump(results, open(out, "w"), indent=2) - print(f"\nSaved to {out}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/full_comparison.py b/benchmarks/full_comparison.py deleted file mode 100644 index cd92fed..0000000 --- a/benchmarks/full_comparison.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Full BAliBASE comparison: all kalign modes + external tools (cached). - -Shows ALL metrics: SP(=Recall), Precision, F1, TC, Time — overall and per-category. -""" - -import json -import statistics -import tempfile -import time -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path - -import kalign -from benchmarks.datasets import get_cases -from benchmarks.scoring import parse_balibase_xml - - -def _score_case(case, output_path): - xml_path = case.reference.with_suffix(".xml") - if xml_path.exists(): - mask = parse_balibase_xml(xml_path) - return kalign.compare_detailed( - str(case.reference), str(output_path), column_mask=mask - ) - return kalign.compare_detailed(str(case.reference), str(output_path)) - - -def _run_one(args): - case, config_name, kwargs = args - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - t0 = time.perf_counter() - kalign.align_file_to_file( - str(case.unaligned), - tmp_path, - format="fasta", - seq_type=case.seq_type, - **kwargs, - ) - wall_time = time.perf_counter() - t0 - scores = _score_case(case, tmp_path) - return { - "family": case.family, - "dataset": case.dataset, - "config": config_name, - **scores, - "wall_time": wall_time, - } - except Exception as e: - return { - "family": case.family, - "dataset": case.dataset, - "config": config_name, - "error": str(e), - } - finally: - Path(tmp_path).unlink(missing_ok=True) - - -def _print_table(title, display_order, summary_data, configs): - """Print one table with ALL metrics: SP, Prec, F1, TC, Time.""" - print(f"\n{'=' * 100}") - print(f" {title}") - print(f"{'=' * 100}") - print( - f"{'Method':<24} {'SP':>8} {'Prec':>8} {'F1':>8} {'TC':>8}" - f" {'Time':>8} {'N':>4}" - ) - print("-" * 100) - for name in display_order: - if name not in summary_data: - continue - sp, prec, f1, tc, t, n = summary_data[name] - marker = " " if name in configs else "*" - t_str = f"{t:.0f}s" if t is not None else "—" - print( - f"{marker}{name:<23} {sp:>8.3f} {prec:>8.3f} {f1:>8.3f}" - f" {tc:>8.3f} {t_str:>8} {n:>4}" - ) - print("-" * 100) - print(" SP = Sum-of-Pairs recall (BAliBASE convention)") - print(" * = external tool (cached results)") - - -def main(): - cases = get_cases("balibase") - print(f"BAliBASE: {len(cases)} cases\n") - - # Kalign configurations to test - configs = { - "baseline(vsm=0)": {"vsm_amax": 0.0}, - "+vsm": {"vsm_amax": 2.0}, - "+vsm+ref": {"vsm_amax": 2.0, "refine": "confident"}, - "+vsm+ref+sw": { - "vsm_amax": 2.0, - "refine": "confident", - "seq_weights": 1.0, - }, - "+vsm+ref+sw+c5": { - "vsm_amax": 2.0, - "refine": "confident", - "seq_weights": 1.0, - "consistency": 5, - "consistency_weight": 2.0, - }, - "ens3+vsm": { - "vsm_amax": 2.0, - "ensemble": 3, - }, - "ens3+vsm+ref": { - "vsm_amax": 2.0, - "ensemble": 3, - "refine": "confident", - }, - "ens3+vsm+ref+ra1": { - "vsm_amax": 2.0, - "ensemble": 3, - "refine": "confident", - "realign": 1, - }, - "ens3+vsm+ref+ra1+c5": { - "vsm_amax": 2.0, - "ensemble": 3, - "refine": "confident", - "realign": 1, - "consistency": 5, - "consistency_weight": 2.0, - }, - } - - tasks = [(c, name, kw) for c in cases for name, kw in configs.items()] - print(f"{len(configs)} configs x {len(cases)} cases = {len(tasks)} tasks\n") - - results = [] - done = 0 - t0 = time.perf_counter() - with ProcessPoolExecutor(max_workers=12) as pool: - futures = {pool.submit(_run_one, t): t for t in tasks} - for f in as_completed(futures): - done += 1 - r = f.result() - results.append(r) - if done % 200 == 0: - elapsed = time.perf_counter() - t0 - print(f" {done}/{len(tasks)} ({elapsed:.0f}s)") - - elapsed = time.perf_counter() - t0 - print(f"\nKalign runs done in {elapsed:.0f}s\n") - - # Load cached external results - ext_path = Path("benchmarks/data/external_balibase.json") - if ext_path.exists(): - ext_data = json.load(open(ext_path)) - for r in ext_data: - r["config"] = r.pop("method") - results.extend(ext_data) - ext_methods = set(r["config"] for r in ext_data) - print(f"Loaded {len(ext_data)} cached external results: {ext_methods}\n") - - # Group by config - groups = defaultdict(list) - for r in results: - if "error" not in r: - groups[r["config"]].append(r) - - # Display order: kalign configs first, then external sorted - display_order = list(configs.keys()) + sorted( - k for k in groups if k not in configs - ) - - # Compute overall summary: (sp, prec, f1, tc, total_time, count) - overall = {} - for name in display_order: - entries = groups.get(name, []) - if not entries: - continue - sp = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - t = sum(r["wall_time"] for r in entries) - overall[name] = (sp, prec, f1, tc, t, len(entries)) - - # Print overall table - _print_table("OVERALL (218 cases)", display_order, overall, configs) - - # Group results by category - categories = ["RV11", "RV12", "RV20", "RV30", "RV40", "RV50"] - by_cat = defaultdict(lambda: defaultdict(list)) - for r in results: - if "error" not in r: - cat = r["dataset"].replace("balibase_", "") - by_cat[cat][r["config"]].append(r) - - # Per-category tables — each with ALL metrics - for cat in categories: - cat_summary = {} - for name in display_order: - entries = by_cat[cat].get(name, []) - if not entries: - continue - sp = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - t = sum(r["wall_time"] for r in entries) - cat_summary[name] = (sp, prec, f1, tc, t, len(entries)) - _print_table(f"{cat} ({len(by_cat[cat].get(display_order[0], []))} cases)", display_order, cat_summary, configs) - - -if __name__ == "__main__": - main() diff --git a/benchmarks/make_summary_figure.py b/benchmarks/make_summary_figure.py deleted file mode 100644 index 99d9d80..0000000 --- a/benchmarks/make_summary_figure.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python3 -"""Generate a summary figure from downstream benchmark results. - -Produces a 2x2 grid of line plots showing method performance vs difficulty, -with the "true" alignment as a dashed black ceiling line. - -- Panel A: Phylo accuracy (nRF) vs tree_depth -- Panel B: Positive selection (F1) vs n_taxa -- Panel C: Alignment accuracy (SP) vs tree_depth -- Panel D: Alignment accuracy (SP) vs indel_rate -""" - -import json -import re -from collections import defaultdict -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - -RESULTS_DIR = Path("benchmarks/results") - -# Method display order (true last — rendered as dashed black) -METHOD_ORDER = [ - "kalign", "kalign_ens3", - "mafft", "muscle", "clustalo", "true", -] -METHOD_LABELS = { - "kalign": "Kalign", - "kalign_ens3": "Kalign ens3", - "mafft": "MAFFT", - "muscle": "MUSCLE", - "clustalo": "Clustal Omega", - "true": "True alignment", -} -METHOD_COLORS = { - "kalign": "#1f77b4", - "kalign_ens3": "#2ca02c", - "mafft": "#9467bd", - "muscle": "#8c564b", - "clustalo": "#7f7f7f", - "true": "#000000", -} - - -def load_cases(pipeline: str) -> list[dict]: - """Load per-case results from the latest.json for a pipeline.""" - path = RESULTS_DIR / pipeline / "latest.json" - with open(path) as f: - data = json.load(f) - return data.get("cases", []) - - -def parse_sim_id(sim_id: str) -> dict: - """Extract parameters from a sim_id string. - - Examples: - WAG_t16_d2.0_ir0.10_il2.0_r0 -> {model:WAG, n_taxa:16, tree_depth:2.0, ...} - M8_t16_d0.5_ir0.05_ps0.10_r0 -> {model:M8, n_taxa:16, tree_depth:0.5, ...} - """ - params = {} - m = re.match(r"^(\w+)_t(\d+)_d([\d.]+)_ir([\d.]+)", sim_id) - if m: - params["model"] = m.group(1) - params["n_taxa"] = int(m.group(2)) - params["tree_depth"] = float(m.group(3)) - params["indel_rate"] = float(m.group(4)) - - # Protein: _il{mean}_r{rep} - m_il = re.search(r"_il([\d.]+)", sim_id) - if m_il: - params["indel_length_mean"] = float(m_il.group(1)) - - # Codon: _ps{frac}_r{rep} - m_ps = re.search(r"_ps([\d.]+)", sim_id) - if m_ps: - params["psel_fraction"] = float(m_ps.group(1)) - - m_r = re.search(r"_r(\d+)$", sim_id) - if m_r: - params["replicate"] = int(m_r.group(1)) - - return params - - -def group_by(cases: list[dict], param_key: str, metric_key: str): - """Group per-case results by a sim_id parameter. - - Returns {method: {x_value: [metric_values]}}. - """ - grouped = defaultdict(lambda: defaultdict(list)) - for c in cases: - if "error" in c: - continue - sim_params = parse_sim_id(c.get("sim_id", "")) - if param_key not in sim_params: - continue - x_val = sim_params[param_key] - method = c["method"] - val = c.get(metric_key) - if val is not None and not (isinstance(val, float) and np.isnan(val)): - grouped[method][x_val].append(val) - return grouped - - -def style_ax(ax, hint=""): - """Apply clean Nature-style aesthetics.""" - ax.spines["top"].set_visible(False) - ax.spines["right"].set_visible(False) - ax.tick_params(labelsize=9) - if hint: - ax.annotate( - hint, xy=(0.98, 0.95), xycoords="axes fraction", - ha="right", va="top", fontsize=8, fontstyle="italic", color="gray", - ) - - -def plot_lines(ax, grouped, methods, xlabel, ylabel): - """Plot lines with error ribbons for each method. - - Parameters - ---------- - grouped : dict - {method: {x_value: [metric_values]}} - methods : list[str] - Method names in plot order. - """ - for method in methods: - if method not in grouped: - continue - data = grouped[method] - xs = sorted(data.keys()) - means = [] - ses = [] - for x in xs: - vals = np.asarray(data[x], dtype=float) - means.append(vals.mean()) - ses.append(vals.std() / max(1, np.sqrt(len(vals)))) - - means = np.asarray(means) - ses = np.asarray(ses) - - color = METHOD_COLORS.get(method, "#333333") - label = METHOD_LABELS.get(method, method) - linestyle = "--" if method == "true" else "-" - linewidth = 1.5 if method != "true" else 2.0 - marker = "o" if method != "true" else "" - - ax.plot( - xs, means, color=color, linestyle=linestyle, - linewidth=linewidth, marker=marker, markersize=4, - label=label, zorder=3 if method != "true" else 2, - ) - ax.fill_between( - xs, means - ses, means + ses, - color=color, alpha=0.12, zorder=1, - ) - - ax.set_xlabel(xlabel, fontsize=10) - ax.set_ylabel(ylabel, fontsize=10) - - -def main(): - fig, axes = plt.subplots(2, 2, figsize=(14, 10)) - fig.suptitle( - "Kalign 3.5 — Downstream Benchmarks by Difficulty", - fontsize=16, fontweight="bold", y=0.98, - ) - - # Determine which methods are present in results - available = set() - for pipeline in ("phylo_accuracy", "positive_selection", "calibration"): - try: - cases = load_cases(pipeline) - for c in cases: - if "error" not in c: - available.add(c["method"]) - except FileNotFoundError: - pass - methods = [m for m in METHOD_ORDER if m in available] - - # ── Panel A: Phylo accuracy (nRF) vs tree_depth ────────────────── - ax = axes[0, 0] - try: - cases = load_cases("phylo_accuracy") - grouped = group_by(cases, "tree_depth", "nrf") - plot_lines(ax, grouped, methods, "Tree depth", "Normalized RF distance") - style_ax(ax, "lower = better") - ax.set_title("A) Phylogenetic tree accuracy", fontweight="bold", loc="left", fontsize=11) - except FileNotFoundError: - ax.text(0.5, 0.5, "No phylo_accuracy results", transform=ax.transAxes, ha="center") - style_ax(ax) - - # ── Panel B: Positive selection (F1) vs n_taxa ─────────────────── - ax = axes[0, 1] - try: - cases = load_cases("positive_selection") - grouped = group_by(cases, "n_taxa", "f1") - plot_lines(ax, grouped, methods, "Number of taxa", "F1 score") - style_ax(ax, "higher = better") - ax.set_title("B) Positive selection detection (FUBAR)", fontweight="bold", loc="left", fontsize=11) - except FileNotFoundError: - ax.text(0.5, 0.5, "No positive_selection results", transform=ax.transAxes, ha="center") - style_ax(ax) - - # ── Panel C: Alignment accuracy (SP) vs tree_depth ─────────────── - ax = axes[1, 0] - try: - cases = load_cases("phylo_accuracy") - grouped = group_by(cases, "tree_depth", "sp_score") - plot_lines(ax, grouped, methods, "Tree depth", "SP score vs true alignment") - style_ax(ax, "higher = better") - ax.set_title("C) Alignment accuracy vs tree depth", fontweight="bold", loc="left", fontsize=11) - except FileNotFoundError: - ax.text(0.5, 0.5, "No phylo_accuracy results", transform=ax.transAxes, ha="center") - style_ax(ax) - - # ── Panel D: Alignment accuracy (SP) vs indel_rate ─────────────── - ax = axes[1, 1] - try: - cases = load_cases("phylo_accuracy") - grouped = group_by(cases, "indel_rate", "sp_score") - plot_lines(ax, grouped, methods, "Indel rate", "SP score vs true alignment") - style_ax(ax, "higher = better") - ax.set_title("D) Alignment accuracy vs indel rate", fontweight="bold", loc="left", fontsize=11) - except FileNotFoundError: - ax.text(0.5, 0.5, "No phylo_accuracy results", transform=ax.transAxes, ha="center") - style_ax(ax) - - # Add legend (shared across panels) - handles, labels = axes[0, 0].get_legend_handles_labels() - if handles: - fig.legend( - handles, labels, loc="lower center", - ncol=min(len(handles), 7), fontsize=9, - frameon=False, bbox_to_anchor=(0.5, 0.0), - ) - - plt.tight_layout(rect=[0, 0.04, 1, 0.95]) - out = Path("benchmarks/figures/downstream_summary.png") - out.parent.mkdir(parents=True, exist_ok=True) - fig.savefig(out, dpi=200, bbox_inches="tight", facecolor="white") - print(f"Saved to {out}") - plt.close() - - -if __name__ == "__main__": - main() diff --git a/benchmarks/mumsa_plots.py b/benchmarks/mumsa_plots.py deleted file mode 100644 index 02c531e..0000000 --- a/benchmarks/mumsa_plots.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Generate publication figures for MUMSA consensus precision analysis. - -Reads results from benchmarks/results/mumsa_precision.json (produced by -mumsa_precision.py) and generates two figures: - - Figure 1: Precision-recall tradeoff curve showing consensus support - thresholds vs single-tool baselines. - Figure 2: Per-category precision bar chart at selected thresholds - alongside external tools. - -Usage: - python -m benchmarks.mumsa_plots [--output-dir figures/] - -Only requires matplotlib + json (no kalign dependency). -""" - -import argparse -import json -import statistics -from collections import defaultdict -from pathlib import Path - -import matplotlib -matplotlib.use("Agg") -import matplotlib.pyplot as plt - - -# Consistent colours -COLORS = { - "consensus": "#2166ac", - "kalign_baseline": "#888888", - "ensemble": "#4daf4a", - "mafft": "#e41a1c", - "muscle": "#ff7f00", - "clustalo": "#984ea3", -} - -TOOL_LABELS = { - "kalign_baseline": "kalign", - "ensemble": "kalign ensemble", - "mafft": "MAFFT", - "muscle": "MUSCLE", - "clustalo": "Clustal Omega", -} - - -def load_results(json_path): - data = json.load(open(json_path)) - return data["results"], data.get("n_runs", 8) - - -def aggregate(results, group_key="method"): - """Group results by method, return {method: {metric: mean_value}}.""" - groups = defaultdict(list) - for r in results: - groups[r[group_key]].append(r) - agg = {} - for method, entries in groups.items(): - agg[method] = { - "recall": statistics.mean(e["recall"] for e in entries), - "precision": statistics.mean(e["precision"] for e in entries), - "f1": statistics.mean(e["f1"] for e in entries), - "tc": statistics.mean(e["tc"] for e in entries), - "n": len(entries), - } - return agg - - -def aggregate_by_category(results): - """Group results by (category, method).""" - groups = defaultdict(list) - for r in results: - cat = r["dataset"].replace("balibase_", "") - groups[(cat, r["method"])].append(r) - agg = {} - for (cat, method), entries in groups.items(): - agg[(cat, method)] = { - "recall": statistics.mean(e["recall"] for e in entries), - "precision": statistics.mean(e["precision"] for e in entries), - "f1": statistics.mean(e["f1"] for e in entries), - "tc": statistics.mean(e["tc"] for e in entries), - "n": len(entries), - } - return agg - - -def figure_precision_vs_support(results, n_runs, output_path): - """Precision and recall as a function of consensus support threshold, - with horizontal reference lines for external tools and kalign baseline.""" - agg = aggregate(results) - - fig, ax = plt.subplots(figsize=(6, 4.5)) - - # Consensus curves - consensus_methods = sorted( - [m for m in agg if m.startswith("consensus_ms")], - key=lambda m: int(m.replace("consensus_ms", "")) - ) - support_vals = [int(m.replace("consensus_ms", "")) for m in consensus_methods] - prec = [agg[m]["precision"] for m in consensus_methods] - rec = [agg[m]["recall"] for m in consensus_methods] - - ax.plot(support_vals, prec, "o-", color=COLORS["consensus"], linewidth=2.5, - markersize=7, zorder=5, label="Consensus precision") - ax.plot(support_vals, rec, "s--", color=COLORS["consensus"], linewidth=1.5, - markersize=5, alpha=0.5, zorder=4, label="Consensus recall") - - # Reference lines for external tools and baselines (precision only) - ref_methods = [ - ("kalign_baseline", COLORS["kalign_baseline"], "kalign"), - ("ensemble", COLORS["ensemble"], "kalign ensemble"), - ("mafft", COLORS["mafft"], "MAFFT"), - ("muscle", COLORS["muscle"], "MUSCLE"), - ("clustalo", COLORS["clustalo"], "Clustal Omega"), - ] - for method, color, label in ref_methods: - if method in agg: - ax.axhline(agg[method]["precision"], color=color, linestyle=":", - linewidth=1.5, alpha=0.8, label=f"{label} precision") - - ax.set_xlabel("Minimum support threshold (s)", fontsize=11) - ax.set_ylabel("Score", fontsize=11) - ax.set_title("Consensus precision increases with support threshold", fontsize=11) - ax.set_xticks(support_vals) - ax.legend(fontsize=7.5, loc="center left", bbox_to_anchor=(1.02, 0.5), - framealpha=0.9) - ax.set_ylim(0.35, 1.0) - ax.grid(True, alpha=0.3) - - fig.tight_layout() - fig.savefig(output_path, dpi=300, bbox_inches="tight") - plt.close(fig) - print(f"Saved {output_path}") - - -def figure_precision_by_category(results, n_runs, output_path): - """Per-category grouped bar chart of precision at selected thresholds.""" - agg = aggregate_by_category(results) - - categories = sorted({r["dataset"].replace("balibase_", "") for r in results}) - - # Methods to show: baseline, ms=3, ms=n_runs, ensemble, mafft, muscle, clustalo - mid = max(1, n_runs // 2) - bar_methods = [ - ("kalign_baseline", "kalign", COLORS["kalign_baseline"]), - (f"consensus_ms{mid}", f"consensus s={mid}", "#6baed6"), - (f"consensus_ms{n_runs}", f"consensus s={n_runs}", COLORS["consensus"]), - ("ensemble", "ensemble", COLORS["ensemble"]), - ("mafft", "MAFFT", COLORS["mafft"]), - ("muscle", "MUSCLE", COLORS["muscle"]), - ("clustalo", "Clustal\u03a9", COLORS["clustalo"]), - ] - - n_cats = len(categories) - n_bars = len(bar_methods) - bar_width = 0.11 - x = range(n_cats) - - fig, ax = plt.subplots(figsize=(9, 4.5)) - - for bi, (method, label, color) in enumerate(bar_methods): - vals = [] - for cat in categories: - key = (cat, method) - vals.append(agg[key]["precision"] if key in agg else 0) - offsets = [xi + (bi - n_bars / 2 + 0.5) * bar_width for xi in x] - ax.bar(offsets, vals, bar_width, label=label, color=color, - edgecolor="white", linewidth=0.3) - - ax.set_xlabel("BAliBASE category", fontsize=11) - ax.set_ylabel("Precision", fontsize=11) - ax.set_title("Precision by category and method", fontsize=11) - ax.set_xticks(list(x)) - ax.set_xticklabels(categories, fontsize=10) - ax.legend(fontsize=7.5, ncol=4, loc="upper center", - bbox_to_anchor=(0.5, -0.12), framealpha=0.9) - ax.set_ylim(0, 1.05) - ax.grid(True, axis="y", alpha=0.3) - - fig.tight_layout() - fig.savefig(output_path, dpi=300, bbox_inches="tight") - plt.close(fig) - print(f"Saved {output_path}") - - -def main(): - parser = argparse.ArgumentParser( - description="Generate MUMSA precision figures from saved results." - ) - parser.add_argument( - "--input", default="benchmarks/results/mumsa_precision.json", - help="Path to results JSON (default: benchmarks/results/mumsa_precision.json)" - ) - parser.add_argument( - "--output-dir", default="benchmarks/figures", - help="Directory for output figures (default: benchmarks/figures/)" - ) - parser.add_argument( - "--format", default="pdf", choices=["pdf", "png", "svg"], - help="Output format (default: pdf)" - ) - args = parser.parse_args() - - results, n_runs = load_results(args.input) - - out_dir = Path(args.output_dir) - out_dir.mkdir(parents=True, exist_ok=True) - - figure_precision_vs_support( - results, n_runs, - out_dir / f"mumsa_precision_vs_support.{args.format}", - ) - figure_precision_by_category( - results, n_runs, - out_dir / f"mumsa_precision_by_category.{args.format}", - ) - - -if __name__ == "__main__": - main() diff --git a/benchmarks/mumsa_precision.py b/benchmarks/mumsa_precision.py deleted file mode 100644 index ea24974..0000000 --- a/benchmarks/mumsa_precision.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Verify MUMSA claim: consensus alignments at higher support thresholds -have higher precision (fraction of aligned residue pairs that are correct). - -Runs kalign ensemble_consensus at min_support = 1..N for each BAliBASE case, -scores against references, and compares precision with baseline/external tools. -""" - -import argparse -import json -import statistics -import tempfile -import time -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path - -import kalign -from .datasets import get_cases -from .scoring import parse_balibase_xml - - -def score_case(case, output_path): - """Score a test alignment against the BAliBASE reference. - Returns dict with recall, precision, f1, tc.""" - xml_path = case.reference.with_suffix(".xml") - if xml_path.exists(): - mask = parse_balibase_xml(xml_path) - return kalign.compare_detailed( - str(case.reference), str(output_path), column_mask=mask - ) - return kalign.compare_detailed(str(case.reference), str(output_path)) - - -def _run_one_case(case, n_runs, max_support): - """Process a single BAliBASE case: run all support thresholds + baseline + ensemble. - Returns list of result dicts.""" - results = [] - - for ms in range(1, max_support + 1): - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - kalign.align_file_to_file( - str(case.unaligned), tmp_path, format="fasta", - seq_type=case.seq_type, ensemble=n_runs, min_support=ms, - ) - scores = score_case(case, tmp_path) - results.append({ - "family": case.family, "dataset": case.dataset, - "method": f"consensus_ms{ms}", "min_support": ms, - "n_runs": n_runs, - "recall": scores["recall"], "precision": scores["precision"], - "f1": scores["f1"], "tc": scores["tc"], - }) - except Exception as e: - results.append({ - "family": case.family, "dataset": case.dataset, - "method": f"consensus_ms{ms}", "min_support": ms, - "n_runs": n_runs, - "recall": 0, "precision": 0, "f1": 0, "tc": 0, - "error": str(e), - }) - finally: - Path(tmp_path).unlink(missing_ok=True) - - # Normal ensemble (no consensus) - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - kalign.align_file_to_file( - str(case.unaligned), tmp_path, format="fasta", - seq_type=case.seq_type, ensemble=n_runs, - ) - scores = score_case(case, tmp_path) - results.append({ - "family": case.family, "dataset": case.dataset, - "method": "ensemble", "min_support": 0, "n_runs": n_runs, - "recall": scores["recall"], "precision": scores["precision"], - "f1": scores["f1"], "tc": scores["tc"], - }) - except Exception: - pass - finally: - Path(tmp_path).unlink(missing_ok=True) - - # Baseline kalign - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - kalign.align_file_to_file( - str(case.unaligned), tmp_path, format="fasta", - seq_type=case.seq_type, - ) - scores = score_case(case, tmp_path) - results.append({ - "family": case.family, "dataset": case.dataset, - "method": "kalign_baseline", "min_support": 0, "n_runs": 0, - "recall": scores["recall"], "precision": scores["precision"], - "f1": scores["f1"], "tc": scores["tc"], - }) - except Exception: - pass - finally: - Path(tmp_path).unlink(missing_ok=True) - - return results - - -def _worker(args): - """Picklable worker for ProcessPoolExecutor.""" - case, n_runs, max_support = args - return _run_one_case(case, n_runs, max_support) - - -def run_consensus_sweep(cases, n_runs=8, max_support=None, categories=None, parallel=1): - """Run ensemble consensus at each support threshold for each case.""" - if max_support is None: - max_support = n_runs - - # Filter cases - filtered = [] - for case in cases: - if categories and not any(cat in case.dataset.upper() for cat in categories): - continue - filtered.append(case) - - all_results = [] - - if parallel > 1: - tasks = [(case, n_runs, max_support) for case in filtered] - done = 0 - with ProcessPoolExecutor(max_workers=parallel) as pool: - futures = {pool.submit(_worker, t): t[0].family for t in tasks} - for future in as_completed(futures): - done += 1 - fam = futures[future] - try: - results = future.result() - all_results.extend(results) - print(f" [{done}/{len(filtered)}] {fam} done") - except Exception as e: - print(f" [{done}/{len(filtered)}] {fam} FAILED: {e}") - else: - for ci, case in enumerate(filtered): - print(f" [{ci+1}/{len(filtered)}] {case.family} ({case.dataset})", end="", flush=True) - results = _run_one_case(case, n_runs, max_support) - all_results.extend(results) - print(" done") - - return all_results - - -def load_external_scores(json_path="benchmarks/results/full_comparison.json"): - """Load external tool scores (mafft, muscle, clustalo) from saved results.""" - p = Path(json_path) - if not p.exists(): - return {} - data = json.load(open(p)) - ext = {} - for r in data["results"]: - if r["method"] in ("mafft", "muscle", "clustalo"): - key = (r["family"], r["method"]) - ext[key] = { - "recall": r.get("recall", 0), - "precision": r.get("precision", 0), - "f1": r.get("f1", 0), - "tc": r.get("tc", 0), - } - return ext - - -def summarize(results, external_scores=None): - """Print summary table of precision across support thresholds.""" - by_method = defaultdict(list) - by_method_cat = defaultdict(lambda: defaultdict(list)) - - for r in results: - by_method[r["method"]].append(r) - cat = r["dataset"].replace("balibase_", "") - by_method_cat[r["method"]][cat].append(r) - - if external_scores: - families = {r["family"] for r in results} - family_datasets = {r["family"]: r["dataset"] for r in results} - for tool in ("mafft", "muscle", "clustalo"): - for fam in families: - key = (fam, tool) - if key in external_scores: - s = external_scores[key] - ds = family_datasets.get(fam, "") - cat = ds.replace("balibase_", "") - entry = {"family": fam, "dataset": ds, **s} - by_method[tool].append(entry) - by_method_cat[tool][cat].append(entry) - - def method_sort_key(m): - if m == "kalign_baseline": - return (0, 0) - if m.startswith("consensus_ms"): - return (1, int(m.replace("consensus_ms", ""))) - if m == "ensemble": - return (2, 0) - return (3, {"mafft": 0, "muscle": 1, "clustalo": 2}.get(m, 9)) - - methods = sorted(by_method.keys(), key=method_sort_key) - - print("\n=== Overall (all categories) ===") - print(f"{'Method':<22} {'Recall':>8} {'Precision':>10} {'F1':>8} {'TC':>8} N") - print("-" * 70) - for m in methods: - entries = by_method[m] - n = len(entries) - rec = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - print(f"{m:<22} {rec:>8.3f} {prec:>10.3f} {f1:>8.3f} {tc:>8.3f} {n}") - - all_cats = sorted({r["dataset"].replace("balibase_", "") for r in results}) - for cat in all_cats: - print(f"\n=== {cat} ===") - print(f"{'Method':<22} {'Recall':>8} {'Precision':>10} {'F1':>8} {'TC':>8} N") - print("-" * 70) - for m in methods: - entries = by_method_cat[m].get(cat, []) - if not entries: - continue - n = len(entries) - rec = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - print(f"{m:<22} {rec:>8.3f} {prec:>10.3f} {f1:>8.3f} {tc:>8.3f} {n}") - - -def main(): - parser = argparse.ArgumentParser(description="MUMSA precision verification") - parser.add_argument("--n-runs", type=int, default=8, - help="Number of ensemble runs (default: 8)") - parser.add_argument("--max-support", type=int, default=None, - help="Maximum min_support threshold (default: n_runs)") - parser.add_argument("--categories", nargs="*", default=None, - help="BAliBASE categories to test (e.g. RV11 RV12)") - parser.add_argument("--max-cases", type=int, default=0, - help="Limit number of cases (0 = all)") - parser.add_argument("--parallel", "-j", type=int, default=4, - help="Number of parallel workers (default: 4)") - args = parser.parse_args() - - categories = [c.upper() for c in args.categories] if args.categories else None - - cases = get_cases("balibase", max_cases=args.max_cases if args.max_cases else None) - print(f"Running MUMSA precision analysis on {len(cases)} BAliBASE cases") - print(f"Ensemble runs: {args.n_runs}, max support: {args.max_support or args.n_runs}") - print(f"Parallel workers: {args.parallel}") - - results = run_consensus_sweep( - cases, n_runs=args.n_runs, - max_support=args.max_support, - categories=categories, - parallel=args.parallel, - ) - - external = load_external_scores() - summarize(results, external) - - # Save results for plotting - out_dir = Path("benchmarks/results") - out_dir.mkdir(parents=True, exist_ok=True) - out_path = out_dir / "mumsa_precision.json" - # Merge external scores into a flat list alongside our results - ext_list = [] - if external: - families = {r["family"] for r in results} - family_datasets = {r["family"]: r["dataset"] for r in results} - for tool in ("mafft", "muscle", "clustalo"): - for fam in families: - key = (fam, tool) - if key in external: - ext_list.append({ - "family": fam, "dataset": family_datasets.get(fam, ""), - "method": tool, "min_support": 0, "n_runs": 0, - **external[key], - }) - json.dump({"results": results + ext_list, "n_runs": args.n_runs}, - open(out_path, "w"), indent=2) - print(f"\nResults saved to {out_path}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/optimize_ensemble.py b/benchmarks/optimize_ensemble.py deleted file mode 100644 index dc24d24..0000000 --- a/benchmarks/optimize_ensemble.py +++ /dev/null @@ -1,1102 +0,0 @@ -#!/usr/bin/env python3 -"""Multi-objective ensemble hyperparameter optimization for kalign using pymoo. - -Optimizes per-run parameters for kalign's ensemble alignment mode, where N -independent alignments (each with different gap penalties, matrices, and tree -noise) are combined via POAR consensus. - -Uses stratified k-fold cross-validation so NSGA-II optimises on held-out -scores, not training scores. - -Objectives (maximized): - 1. Mean held-out F1 across folds (category-averaged within each fold) - 2. Mean held-out TC across folds (category-averaged within each fold) - -Per-run decision variables (× N runs): - - gpo: gap open penalty [2.0, 15.0] - - gpe: gap extend penalty [0.5, 5.0] - - tgpe: terminal gap extend [0.1, 3.0] - - noise: tree perturbation sigma [0.0, 0.5] - - matrix: substitution matrix {PFASUM43, PFASUM60, CorBLOSUM66} - -Shared decision variables: - - vsm_amax: variable scoring matrix [0.0, 5.0] - - consistency: anchor consistency rounds {0, 1, 2, 3, 5, 8} - - consistency_weight: consistency bonus weight [0.5, 5.0] - - realign: tree-rebuild iterations {0, 1, 2} - - min_support: POAR consensus threshold {0, 1, ..., N} - -Usage: - # Quick test - uv run python -m benchmarks.optimize_ensemble --n-runs 3 --pop-size 20 --n-gen 5 - - # Production (3 runs, Threadripper) - uv run python -m benchmarks.optimize_ensemble \\ - --n-runs 3 --pop-size 100 --n-gen 50 --n-workers 56 - - # Production (5 runs) - uv run python -m benchmarks.optimize_ensemble \\ - --n-runs 5 --pop-size 150 --n-gen 60 --n-workers 56 - - # Production (8 runs) - uv run python -m benchmarks.optimize_ensemble \\ - --n-runs 8 --pop-size 200 --n-gen 80 --n-workers 56 - - # Resume after interrupt - uv run python -m benchmarks.optimize_ensemble \\ - --resume benchmarks/results/ensemble_optim/gen_checkpoint.pkl --n-gen 80 -""" - -import argparse -import os -import pickle -import signal -import sys -import tempfile -import time -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -import numpy as np - -try: - from pymoo.algorithms.moo.nsga2 import NSGA2 # type: ignore[import-untyped] - from pymoo.core.callback import Callback # type: ignore[import-untyped] - from pymoo.core.problem import Problem # type: ignore[import-untyped] - from pymoo.operators.crossover.sbx import SBX # type: ignore[import-untyped] - from pymoo.operators.mutation.pm import PM # type: ignore[import-untyped] - from pymoo.operators.sampling.lhs import LHS # type: ignore[import-untyped] - from pymoo.optimize import minimize # type: ignore[import-untyped] - from pymoo.termination import get_termination # type: ignore[import-untyped] -except ImportError: - print("pymoo not installed. Run: uv pip install pymoo") - sys.exit(1) - -from rich.console import Console # type: ignore[import-untyped] -from rich.layout import Layout # type: ignore[import-untyped] -from rich.live import Live # type: ignore[import-untyped] -from rich.panel import Panel # type: ignore[import-untyped] -from rich.progress import ( # type: ignore[import-untyped] - BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, -) -from rich.table import Table # type: ignore[import-untyped] -from rich.text import Text # type: ignore[import-untyped] - -from kalign._core import ( # type: ignore[import-untyped] - PROTEIN, PROTEIN_PFASUM43, PROTEIN_PFASUM60, REFINE_CONFIDENT, - ensemble_custom_file_to_file, -) -import kalign # type: ignore[import-untyped] - -from .datasets import BenchmarkCase, balibase_cases, balibase_download, balibase_is_available -from .scoring import score_alignment_detailed - -# --------------------------------------------------------------------------- -# Parameter space definition -# --------------------------------------------------------------------------- - -# Per-run continuous: [gpo, gpe, tgpe, noise] × N_RUNS -PER_RUN_CONT_LOWER = np.array([2.0, 0.5, 0.1, 0.0]) -PER_RUN_CONT_UPPER = np.array([15.0, 5.0, 3.0, 0.5]) -PER_RUN_CONT_NAMES = ["gpo", "gpe", "tgpe", "noise"] -N_PER_RUN_CONT = len(PER_RUN_CONT_LOWER) - -# Per-run integer: [matrix] × N_RUNS -# matrix: 0=PFASUM43, 1=PFASUM60, 2=CorBLOSUM66 -N_PER_RUN_INT = 1 # just matrix - -# Shared continuous: [vsm_amax, consistency_weight] -SHARED_CONT_LOWER = np.array([0.0, 0.5]) -SHARED_CONT_UPPER = np.array([5.0, 5.0]) -SHARED_CONT_NAMES = ["vsm_amax", "consistency_weight"] -N_SHARED_CONT = len(SHARED_CONT_LOWER) - -# Shared integer: [consistency, realign, min_support] -# consistency: 0-5 maps via CONSISTENCY_MAP -# realign: 0-2 -# min_support: 0-N_RUNS (0=auto) -N_SHARED_INT = 3 # consistency, realign, min_support - -CONSISTENCY_MAP = [0, 1, 2, 3, 5, 8] -MATRIX_MAP = [PROTEIN_PFASUM43, PROTEIN_PFASUM60, PROTEIN] -MATRIX_NAMES = {PROTEIN_PFASUM43: "P43", PROTEIN_PFASUM60: "P60", PROTEIN: "CB66"} - - -def get_var_counts(n_runs: int): - """Return (n_cont, n_int, n_var) for a given number of ensemble runs.""" - n_cont = n_runs * N_PER_RUN_CONT + N_SHARED_CONT - n_int = n_runs * N_PER_RUN_INT + N_SHARED_INT - n_var = n_cont + n_int - return n_cont, n_int, n_var - - -def get_bounds(n_runs: int): - """Return (xl, xu) arrays for the full decision vector.""" - # Continuous: [per_run_cont × N_RUNS, shared_cont] - cont_lower = np.concatenate([np.tile(PER_RUN_CONT_LOWER, n_runs), SHARED_CONT_LOWER]) - cont_upper = np.concatenate([np.tile(PER_RUN_CONT_UPPER, n_runs), SHARED_CONT_UPPER]) - - # Integer: [matrix × N_RUNS, consistency, realign, min_support] - int_lower = np.zeros(n_runs * N_PER_RUN_INT + N_SHARED_INT) - int_upper = np.concatenate([ - np.full(n_runs, 2.0), # matrix: 0-2 - [len(CONSISTENCY_MAP) - 1], # consistency: 0-5 - [2.0], # realign: 0-2 - [float(n_runs)], # min_support: 0-N - ]) - - xl = np.concatenate([cont_lower, int_lower]) - xu = np.concatenate([cont_upper, int_upper]) - return xl, xu - - -def decode_ensemble_params(x, n_runs: int): - """Decode a decision vector into ensemble parameter dict. - - Returns dict with: - run_gpo, run_gpe, run_tgpe, run_noise: lists of float (length n_runs) - run_types: list of int (length n_runs) - vsm_amax, consistency_weight: float - consistency, realign, min_support: int - """ - n_cont, n_int, _ = get_var_counts(n_runs) - cont = x[:n_cont] - ints = np.round(x[n_cont:]).astype(int) - - # Per-run continuous - run_gpo = [] - run_gpe = [] - run_tgpe = [] - run_noise = [] - for k in range(n_runs): - offset = k * N_PER_RUN_CONT - run_gpo.append(float(cont[offset + 0])) - run_gpe.append(float(cont[offset + 1])) - run_tgpe.append(float(cont[offset + 2])) - run_noise.append(float(cont[offset + 3])) - - # Shared continuous - shared_offset = n_runs * N_PER_RUN_CONT - vsm_amax = float(cont[shared_offset + 0]) - consistency_weight = float(cont[shared_offset + 1]) - - # Per-run integer (matrix) - run_types = [] - for k in range(n_runs): - matrix_idx = int(np.clip(ints[k], 0, len(MATRIX_MAP) - 1)) - run_types.append(MATRIX_MAP[matrix_idx]) - - # Shared integer - shared_int_offset = n_runs * N_PER_RUN_INT - consistency_idx = int(np.clip(ints[shared_int_offset + 0], 0, len(CONSISTENCY_MAP) - 1)) - realign = int(np.clip(ints[shared_int_offset + 1], 0, 2)) - min_support = int(np.clip(ints[shared_int_offset + 2], 0, n_runs)) - - return { - "run_gpo": run_gpo, - "run_gpe": run_gpe, - "run_tgpe": run_tgpe, - "run_noise": run_noise, - "run_types": run_types, - "vsm_amax": vsm_amax, - "consistency_weight": consistency_weight, - "consistency": CONSISTENCY_MAP[consistency_idx], - "realign": realign, - "min_support": min_support, - } - - -def format_run_short(params, k): - """Short string for one run's params.""" - mat = MATRIX_NAMES.get(params["run_types"][k], "?") - return (f"gpo={params['run_gpo'][k]:.1f} gpe={params['run_gpe'][k]:.2f} " - f"tgpe={params['run_tgpe'][k]:.2f} n={params['run_noise'][k]:.2f} {mat}") - - -def format_ensemble_short(params): - """Compact summary of ensemble params.""" - n_runs = len(params["run_gpo"]) - runs = [] - for k in range(n_runs): - mat = MATRIX_NAMES.get(params["run_types"][k], "?") - runs.append(f"R{k}:{params['run_gpo'][k]:.1f}/{mat}") - run_str = " ".join(runs) - shared_str = (f"vsm={params['vsm_amax']:.1f} c={params['consistency']} " - f"cw={params['consistency_weight']:.1f} re={params['realign']} " - f"ms={params['min_support']}") - return f"{run_str} | {shared_str}" - - -def format_ensemble_long(params): - """Verbose multi-line summary.""" - lines = [] - n_runs = len(params["run_gpo"]) - for k in range(n_runs): - lines.append(f" Run {k}: {format_run_short(params, k)}") - lines.append(f" Shared: vsm={params['vsm_amax']:.2f} " - f"cons={params['consistency']} cw={params['consistency_weight']:.2f} " - f"re={params['realign']} ms={params['min_support']}") - return "\n".join(lines) - - -# --------------------------------------------------------------------------- -# Stratified k-fold CV (reused from optimize_params) -# --------------------------------------------------------------------------- - -def stratified_kfold(cases: List[BenchmarkCase], k: int, seed: int = 42 - ) -> List[Tuple[List[BenchmarkCase], List[BenchmarkCase]]]: - """Split cases into k stratified folds by dataset (RV category).""" - rng = np.random.RandomState(seed) - - by_cat: Dict[str, List[BenchmarkCase]] = defaultdict(list) - for c in cases: - by_cat[c.dataset].append(c) - - fold_assignments: Dict[str, int] = {} - for cat_cases in by_cat.values(): - indices = list(range(len(cat_cases))) - rng.shuffle(indices) - for rank, idx in enumerate(indices): - fold_assignments[cat_cases[idx].family] = rank % k - - folds = [] - for fold_idx in range(k): - test = [c for c in cases if fold_assignments[c.family] == fold_idx] - train = [c for c in cases if fold_assignments[c.family] != fold_idx] - folds.append((train, test)) - - return folds - - -# --------------------------------------------------------------------------- -# Evaluation -# --------------------------------------------------------------------------- - -def evaluate_ensemble(params, cases, n_threads=1, quiet=True): - """Run ensemble alignment with given params on all cases, return mean metrics.""" - results_by_cat: Dict[str, list] = {} - total_time = 0.0 - - for case in cases: - with tempfile.TemporaryDirectory() as tmpdir: - output = Path(tmpdir) / f"{case.family}_aln.fasta" - - try: - start = time.perf_counter() - ensemble_custom_file_to_file( - str(case.unaligned), - str(output), - run_gpo=params["run_gpo"], - run_gpe=params["run_gpe"], - run_tgpe=params["run_tgpe"], - run_noise=params["run_noise"], - run_types=params["run_types"], - format="fasta", - seq_type=PROTEIN, - seed=42, - min_support=params["min_support"], - refine=REFINE_CONFIDENT, - vsm_amax=params["vsm_amax"], - realign=params["realign"], - seq_weights=-1.0, - n_threads=n_threads, - consistency_anchors=params["consistency"], - consistency_weight=params["consistency_weight"], - ) - wall_time = time.perf_counter() - start - total_time += wall_time - - detailed = score_alignment_detailed(case.reference, output) - - cat = case.dataset - if cat not in results_by_cat: - results_by_cat[cat] = [] - results_by_cat[cat].append(detailed) - - except Exception as e: - if not quiet: - print(f" WARN: {case.family}: {e}", file=sys.stderr) - - if not results_by_cat: - return {"f1": 0.0, "tc": 0.0, "recall": 0.0, "precision": 0.0, - "wall_time": total_time, "per_category": {}} - - per_cat = {} - for cat, scores in results_by_cat.items(): - per_cat[cat] = { - "f1": np.mean([s["f1"] for s in scores]), - "tc": np.mean([s["tc"] for s in scores]), - "recall": np.mean([s["recall"] for s in scores]), - "precision": np.mean([s["precision"] for s in scores]), - "n": len(scores), - } - - all_f1 = [v["f1"] for v in per_cat.values()] - all_tc = [v["tc"] for v in per_cat.values()] - all_recall = [v["recall"] for v in per_cat.values()] - all_precision = [v["precision"] for v in per_cat.values()] - - return { - "f1": float(np.mean(all_f1)), - "tc": float(np.mean(all_tc)), - "recall": float(np.mean(all_recall)), - "precision": float(np.mean(all_precision)), - "wall_time": total_time, - "per_category": per_cat, - } - - -def evaluate_ensemble_cv(params, folds, n_threads=1, quiet=True): - """Evaluate ensemble params using stratified k-fold CV.""" - fold_f1s = [] - fold_tcs = [] - total_time = 0.0 - - for _, test in folds: - result = evaluate_ensemble(params, test, n_threads, quiet) - fold_f1s.append(result["f1"]) - fold_tcs.append(result["tc"]) - total_time += result["wall_time"] - - return { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, - "fold_tcs": fold_tcs, - "wall_time": total_time, - } - - -# --------------------------------------------------------------------------- -# Rich live dashboard -# --------------------------------------------------------------------------- - -class Dashboard: - """Rich-based live terminal dashboard for ensemble optimization progress.""" - - def __init__(self, n_gen: int, pop_size: int, baseline_f1: float, baseline_tc: float): - self.n_gen = n_gen - self.pop_size = pop_size - self.baseline_f1 = baseline_f1 - self.baseline_tc = baseline_tc - self.console = Console() - - self.current_gen = 0 - self.eval_count = 0 - self.total_evals = n_gen * pop_size - self.gen_start_time = time.time() - self.run_start_time = time.time() - self.current_eval_params: Optional[dict] = None - self.current_eval_idx = 0 - - self.best_f1 = 0.0 - self.best_f1_params: Optional[dict] = None - self.best_tc = 0.0 - self.best_tc_params: Optional[dict] = None - - self.pareto_front: List[dict] = [] - self.gen_history: List[dict] = [] - self.recent_evals: List[dict] = [] - - self.progress = Progress( - SpinnerColumn(), - TextColumn("[bold blue]{task.description}"), - BarColumn(bar_width=40), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeElapsedColumn(), - ) - self.gen_task = self.progress.add_task("Generation", total=n_gen) - self.eval_task = self.progress.add_task(" Eval", total=pop_size) - - self.live = Live(self._build_layout(), console=self.console, refresh_per_second=2) - - def start(self): - self.run_start_time = time.time() - self.live.start() - - def stop(self): - self.live.stop() - - def _delta_str(self, value: float, baseline: float) -> str: - delta = value - baseline - if delta > 0.005: - return f"[bold green]{value:.4f} (+{delta:.4f})[/]" - elif delta < -0.005: - return f"[bold red]{value:.4f} ({delta:.4f})[/]" - else: - return f"[dim]{value:.4f} ({delta:+.4f})[/]" - - def _build_status_panel(self) -> Panel: - elapsed = time.time() - self.run_start_time - gen_elapsed = time.time() - self.gen_start_time - - if self.eval_count > 0: - avg_per_eval = elapsed / self.eval_count - remaining = avg_per_eval * (self.total_evals - self.eval_count) - eta_str = f"{remaining / 60:.0f}m" if remaining > 60 else f"{remaining:.0f}s" - else: - eta_str = "..." - - lines = [ - f"Gen [bold]{self.current_gen}[/]/{self.n_gen} " - f"Eval [bold]{self.eval_count}[/]/{self.total_evals} " - f"Elapsed [bold]{elapsed / 60:.1f}m[/] " - f"ETA [bold]{eta_str}[/] " - f"Gen time [bold]{gen_elapsed:.1f}s[/]", - ] - - if self.current_eval_params: - lines.append(f"[dim]Current: {format_ensemble_short(self.current_eval_params)}[/]") - - return Panel("\n".join(lines), title="Ensemble Optimization", border_style="blue") - - def _build_best_panel(self) -> Panel: - lines = [] - lines.append(f"[dim]Baseline: F1={self.baseline_f1:.4f} TC={self.baseline_tc:.4f}[/]") - lines.append("") - - if self.best_f1_params: - lines.append(f"Best F1: {self._delta_str(self.best_f1, self.baseline_f1)}") - lines.append(f" [dim]{format_ensemble_short(self.best_f1_params)}[/]") - else: - lines.append("[dim]Best F1: (no evaluations yet)[/]") - - lines.append("") - - if self.best_tc_params: - lines.append(f"Best TC: {self._delta_str(self.best_tc, self.baseline_tc)}") - lines.append(f" [dim]{format_ensemble_short(self.best_tc_params)}[/]") - else: - lines.append("[dim]Best TC: (no evaluations yet)[/]") - - return Panel("\n".join(lines), title="Best Solutions", border_style="green") - - def _build_pareto_table(self) -> Panel: - table = Table(show_header=True, expand=True, padding=(0, 1)) - table.add_column("#", style="dim", width=3) - table.add_column("CV F1", justify="right", width=8) - table.add_column("CV TC", justify="right", width=8) - table.add_column("Parameters") - - for i, cfg in enumerate(self.pareto_front[:8]): - f1_style = "bold green" if cfg["f1"] > self.baseline_f1 else "" - tc_style = "bold green" if cfg["tc"] > self.baseline_tc else "" - table.add_row( - str(i), - Text(f"{cfg['f1']:.4f}", style=f1_style), - Text(f"{cfg['tc']:.4f}", style=tc_style), - format_ensemble_short(cfg["params"]), - ) - - return Panel(table, title="Pareto Front", border_style="yellow") - - def _build_trend_panel(self) -> Panel: - if not self.gen_history: - return Panel("[dim]No data yet[/]", title="Trend", border_style="cyan") - - entries = [] - step = max(1, len(self.gen_history) // 8) - for i in range(0, len(self.gen_history), step): - h = self.gen_history[i] - entries.append(f"Gen {h['gen']:>3}: F1={h['best_f1']:.4f}") - if self.gen_history[-1] not in [self.gen_history[i] for i in range(0, len(self.gen_history), step)]: - h = self.gen_history[-1] - entries.append(f"Gen {h['gen']:>3}: F1={h['best_f1']:.4f}") - - return Panel(" ".join(entries), title="Trend", border_style="cyan") - - def _build_recent_panel(self) -> Panel: - if not self.recent_evals: - return Panel("[dim]No evaluations yet[/]", title="Recent", border_style="magenta") - - lines = [] - for ev in self.recent_evals[-5:]: - lines.append(f"F1={ev['f1']:.4f} TC={ev['tc']:.4f} {format_ensemble_short(ev['params'])}") - - return Panel("\n".join(lines), title="Recent", border_style="magenta") - - def _build_layout(self) -> Layout: - layout = Layout() - layout.split_column( - Layout(self._build_status_panel(), name="status", size=4), - Layout(self.progress, name="progress", size=3), - Layout(self._build_best_panel(), name="best", size=9), - Layout(self._build_pareto_table(), name="pareto", size=12), - Layout(self._build_trend_panel(), name="trend", size=3), - Layout(self._build_recent_panel(), name="recent", size=8), - ) - return layout - - def _refresh(self): - self.live.update(self._build_layout()) - - def on_eval_start(self, params, eval_count, idx_in_gen): - self.current_eval_params = params - self.eval_count = eval_count - self.current_eval_idx = idx_in_gen - self.progress.update(self.eval_task, completed=idx_in_gen) - self._refresh() - - def on_eval_end(self, params, cv_result): - f1 = cv_result["f1"] - tc = cv_result["tc"] - - if f1 > self.best_f1: - self.best_f1 = f1 - self.best_f1_params = params - if tc > self.best_tc: - self.best_tc = tc - self.best_tc_params = params - - self.recent_evals.append({"params": params, "f1": f1, "tc": tc}) - if len(self.recent_evals) > 10: - self.recent_evals = self.recent_evals[-10:] - - self._refresh() - - def on_gen_start(self, gen): - self.current_gen = gen - self.gen_start_time = time.time() - self.progress.update(self.gen_task, completed=gen - 1) - self.progress.update(self.eval_task, completed=0) - self._refresh() - - def on_gen_end(self, gen, pareto): - self.current_gen = gen - self.progress.update(self.gen_task, completed=gen) - self.progress.update(self.eval_task, completed=self.pop_size) - - self.pareto_front = sorted(pareto, key=lambda x: -x["f1"]) - - best_f1_in_gen = max(p["f1"] for p in pareto) if pareto else 0.0 - self.gen_history.append({"gen": gen, "best_f1": best_f1_in_gen}) - - self._refresh() - - -# --------------------------------------------------------------------------- -# ProcessPoolExecutor helper -# --------------------------------------------------------------------------- - -def _kill_pool(pool: ProcessPoolExecutor) -> None: - """Forcefully terminate all worker processes in the pool.""" - for pid in list(pool._processes): # noqa: SLF001 - try: - os.kill(pid, signal.SIGTERM) - except OSError: - pass - pool.shutdown(wait=False, cancel_futures=True) - - -# Top-level function for pickling (ProcessPoolExecutor requires this) -def _eval_one_ensemble(args_tuple): - """Evaluate one ensemble parameter vector. Top-level for ProcessPoolExecutor.""" - x, n_runs, folds, n_threads = args_tuple - params = decode_ensemble_params(x, n_runs) - cv_result = evaluate_ensemble_cv(params, folds, n_threads, quiet=True) - return params, cv_result - - -def _eval_one_ensemble_fold(args_tuple): - """Evaluate one (individual, fold) pair for ensemble. Fine-grained parallelism.""" - ind_idx, fold_idx, x, n_runs, test_cases, n_threads = args_tuple - params = decode_ensemble_params(x, n_runs) - result = evaluate_ensemble(params, test_cases, n_threads, quiet=True) - return ind_idx, fold_idx, params, result - - -# --------------------------------------------------------------------------- -# pymoo Problem + Callback -# --------------------------------------------------------------------------- - -class EnsembleCVProblem(Problem): - """Multi-objective optimization for ensemble with stratified CV.""" - - def __init__(self, n_runs, folds, n_threads=1, n_workers=1, - optimize_time=False, dashboard: Optional[Dashboard] = None): - xl, xu = get_bounds(n_runs) - n_cont, n_int, n_var = get_var_counts(n_runs) - n_obj = 3 if optimize_time else 2 - - super().__init__( - n_var=n_var, - n_obj=n_obj, - xl=xl, - xu=xu, - ) - self.n_runs = n_runs - self.folds = folds - self.n_threads = n_threads - self.n_workers = n_workers - self.optimize_time = optimize_time - self.dashboard = dashboard - self.eval_count = 0 - self.history: List[dict] = [] - - def _evaluate(self, X, out, *args, **kwargs): # pyright: ignore[reportUnusedVariable] - F = np.zeros((X.shape[0], self.n_obj)) - - if self.n_workers > 1: - self._evaluate_parallel(X, F) - else: - self._evaluate_serial(X, F) - - out["F"] = F - - def _evaluate_serial(self, X, F): - for i, x in enumerate(X): - params = decode_ensemble_params(x, self.n_runs) - self.eval_count += 1 - - if self.dashboard: - self.dashboard.on_eval_start(params, self.eval_count, i) - - cv_result = evaluate_ensemble_cv(params, self.folds, self.n_threads, quiet=True) - self._record(i, F, params, cv_result) - - def _evaluate_parallel(self, X, F): - """Fine-grained parallelism: submit (individual × fold) jobs.""" - n_folds = len(self.folds) - - jobs = [] - for i, x in enumerate(X): - for fold_idx, (_, test) in enumerate(self.folds): - jobs.append((i, fold_idx, x, self.n_runs, test, self.n_threads)) - - if self.dashboard: - self.dashboard.on_eval_start( - decode_ensemble_params(X[0], self.n_runs), - self.eval_count + 1, 0) - - fold_results: Dict[int, Dict[int, dict]] = defaultdict(dict) - ind_params: Dict[int, dict] = {} - - pool = ProcessPoolExecutor(max_workers=self.n_workers) - try: - futures = {pool.submit(_eval_one_ensemble_fold, j): j[:2] for j in jobs} - completed_individuals = set() - - for future in as_completed(futures): - ind_idx, fold_idx, params, result = future.result() - fold_results[ind_idx][fold_idx] = result - ind_params[ind_idx] = params - - if len(fold_results[ind_idx]) == n_folds: - completed_individuals.add(ind_idx) - self.eval_count += 1 - - fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] - fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] - total_time = sum(fold_results[ind_idx][fi]["wall_time"] for fi in range(n_folds)) - - cv_result = { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, - "fold_tcs": fold_tcs, - "wall_time": total_time, - } - - self._record(ind_idx, F, params, cv_result) - - if self.dashboard: - self.dashboard.on_eval_start( - params, self.eval_count, len(completed_individuals)) - - except KeyboardInterrupt: - _kill_pool(pool) - raise - finally: - pool.shutdown(wait=False) - - def _record(self, i, F, params, cv_result): - F[i, 0] = -cv_result["f1"] - F[i, 1] = -cv_result["tc"] - if self.optimize_time: - F[i, 2] = cv_result["wall_time"] - - self.history.append({ - "eval": self.eval_count, - "params": params, - "cv_result": cv_result, - }) - - if self.dashboard: - self.dashboard.on_eval_end(params, cv_result) - - -class GenerationCallback(Callback): - """pymoo callback: updates dashboard + saves checkpoint after each generation.""" - - def __init__(self, dashboard: Optional[Dashboard] = None, - checkpoint_path: Optional[Path] = None, - problem: Optional[EnsembleCVProblem] = None): - super().__init__() - self.dashboard = dashboard - self.checkpoint_path = checkpoint_path - self.problem = problem - - def notify(self, algorithm): - gen = algorithm.n_gen - - if self.dashboard: - self.dashboard.on_gen_start(gen) - - pareto = [] - if algorithm.opt is not None and len(algorithm.opt) > 0: - n_runs = self.problem.n_runs if self.problem else 3 - for ind in algorithm.opt: - params = decode_ensemble_params(ind.X, n_runs) - pareto.append({ - "params": params, - "f1": -ind.F[0], - "tc": -ind.F[1], - }) - - if self.dashboard: - self.dashboard.on_gen_end(gen, pareto) - - if self.checkpoint_path: - pop = algorithm.pop - ckpt = { - "pop_X": pop.get("X").copy(), - "pop_F": pop.get("F").copy(), - "n_gen_completed": gen, - "history": self.problem.history if self.problem else [], - "pop_size": len(pop), - "n_runs": self.problem.n_runs if self.problem else 3, - } - tmp = self.checkpoint_path.with_suffix(".tmp") - with open(tmp, "wb") as f: - pickle.dump(ckpt, f) - tmp.rename(self.checkpoint_path) - - -def load_checkpoint(path: Path): - """Load a generation checkpoint.""" - with open(path, "rb") as f: - ckpt = pickle.load(f) # noqa: S301 - return (ckpt["pop_X"], ckpt["pop_F"], ckpt["n_gen_completed"], - ckpt.get("history", []), ckpt.get("n_runs", 3)) - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser( - description="Optimize kalign ensemble hyperparameters with NSGA-II + stratified k-fold CV") - parser.add_argument("--n-runs", type=int, default=3, - help="Number of ensemble runs (default: 3)") - parser.add_argument("--pop-size", type=int, default=100, - help="Population size (default: 100)") - parser.add_argument("--n-gen", type=int, default=50, - help="Number of generations (default: 50)") - parser.add_argument("--n-folds", type=int, default=5, - help="Number of CV folds (default: 5)") - parser.add_argument("--n-threads", type=int, default=1, - help="OpenMP threads per alignment (default: 1)") - parser.add_argument("--n-workers", type=int, default=1, - help="Parallel worker processes (default: 1)") - parser.add_argument("--seed", type=int, default=42, - help="Random seed (default: 42)") - parser.add_argument("--optimize-time", action="store_true", - help="Add wall time as 3rd objective") - parser.add_argument("--output-dir", type=str, default="benchmarks/results/ensemble_optim", - help="Output directory") - parser.add_argument("--run-name", type=str, default=None, - help="Name for this run (creates subdirectory, e.g. 'run1_3runs')") - parser.add_argument("--no-dashboard", action="store_true", - help="Disable rich dashboard") - parser.add_argument("--resume", type=str, default=None, - help="Resume from a generation checkpoint file (.pkl)") - args = parser.parse_args() - - console = Console() - n_runs = args.n_runs - - # Dimensions - n_cont, n_int, n_var = get_var_counts(n_runs) - console.print(f"[bold]Ensemble optimizer[/]: {n_runs} runs, " - f"{n_var} decision variables ({n_cont} continuous + {n_int} integer)") - - # Setup - if not balibase_is_available(): - console.print("Downloading BAliBASE...") - balibase_download() - - cases = balibase_cases() - console.print(f"Loaded [bold]{len(cases)}[/] BAliBASE cases") - - cats: Dict[str, int] = {} - for c in cases: - cats[c.dataset] = cats.get(c.dataset, 0) + 1 - for cat, n in sorted(cats.items()): - console.print(f" {cat}: {n} cases") - - k = args.n_folds - folds = stratified_kfold(cases, k, seed=args.seed) - console.print(f"\nStratified [bold]{k}[/]-fold CV:") - for i, (train, test) in enumerate(folds): - test_cats: Dict[str, int] = defaultdict(int) - for c in test: - test_cats[c.dataset] += 1 - cat_str = ", ".join(f"{cat.replace('balibase_', '')}:{n}" - for cat, n in sorted(test_cats.items())) - console.print(f" Fold {i}: {len(test)} test / {len(train)} train ({cat_str})") - - output_dir = Path(args.output_dir) - if args.run_name: - output_dir = output_dir / args.run_name - output_dir.mkdir(parents=True, exist_ok=True) - - # --- Baseline evaluation (current best: ens3+vsm+ref+ra1) --- - console.print(f"\n[bold]Baseline evaluation[/] (ens{n_runs}+vsm+ref+ra1, {k}-fold CV)") - console.print("[dim]This uses the original kalign_ensemble with hardcoded scale factors[/]") - - # Evaluate baseline via the standard ensemble API - baseline_fold_f1s = [] - baseline_fold_tcs = [] - baseline_total_time = 0.0 - for fold_idx, (_, test) in enumerate(folds): - results_by_cat: Dict[str, list] = {} - for case in test: - with tempfile.TemporaryDirectory() as tmpdir: - output = Path(tmpdir) / f"{case.family}_aln.fasta" - try: - start = time.perf_counter() - kalign.align_file_to_file( - str(case.unaligned), str(output), - format="fasta", - ensemble=n_runs, - vsm_amax=2.0, - refine="confident", - realign=1, - ensemble_seed=42, - ) - baseline_total_time += time.perf_counter() - start - detailed = score_alignment_detailed(case.reference, output) - cat = case.dataset - if cat not in results_by_cat: - results_by_cat[cat] = [] - results_by_cat[cat].append(detailed) - except Exception as e: - console.print(f" [yellow]WARN[/]: {case.family}: {e}") - - if results_by_cat: - cat_f1s = [np.mean([s["f1"] for s in v]) for v in results_by_cat.values()] - cat_tcs = [np.mean([s["tc"] for s in v]) for v in results_by_cat.values()] - baseline_fold_f1s.append(float(np.mean(cat_f1s))) - baseline_fold_tcs.append(float(np.mean(cat_tcs))) - - baseline_cv = { - "f1": float(np.mean(baseline_fold_f1s)), - "tc": float(np.mean(baseline_fold_tcs)), - "f1_std": float(np.std(baseline_fold_f1s)), - "tc_std": float(np.std(baseline_fold_tcs)), - "fold_f1s": baseline_fold_f1s, - "fold_tcs": baseline_fold_tcs, - "wall_time": baseline_total_time, - } - console.print(f" CV F1=[bold]{baseline_cv['f1']:.4f}[/]±{baseline_cv['f1_std']:.4f} " - f"CV TC=[bold]{baseline_cv['tc']:.4f}[/]±{baseline_cv['tc_std']:.4f} " - f"time={baseline_cv['wall_time']:.1f}s") - for i, (f1, tc) in enumerate(zip(baseline_cv['fold_f1s'], baseline_cv['fold_tcs'])): - console.print(f" Fold {i}: F1={f1:.4f} TC={tc:.4f}") - - # --- Optimization --- - n_evals = args.pop_size * args.n_gen - est_sec_per_eval = baseline_cv["wall_time"] / k # baseline did k folds - parallelism = max(1, args.n_workers) - est_hours = n_evals * est_sec_per_eval / parallelism / 3600 - - console.print(f"\n[bold]Starting NSGA-II[/]: pop_size={args.pop_size}, n_gen={args.n_gen}, " - f"{k}-fold CV, {n_runs} ensemble runs, " - f"{args.n_workers} worker(s) × {args.n_threads} thread(s)") - console.print(f"Total evaluations: ~{n_evals}") - console.print(f"Estimated time: ~{est_hours:.1f} hours\n") - - use_dashboard = not args.no_dashboard - dashboard = None - - if use_dashboard: - dashboard = Dashboard( - n_gen=args.n_gen, - pop_size=args.pop_size, - baseline_f1=baseline_cv["f1"], - baseline_tc=baseline_cv["tc"], - ) - - problem = EnsembleCVProblem( - n_runs=n_runs, - folds=folds, - n_threads=args.n_threads, - n_workers=args.n_workers, - optimize_time=args.optimize_time, - dashboard=dashboard, - ) - - checkpoint_path = output_dir / "gen_checkpoint.pkl" - callback = GenerationCallback( - dashboard=dashboard, - checkpoint_path=checkpoint_path, - problem=problem, - ) - - # Resume from checkpoint or start fresh - resumed_gen = 0 - if args.resume: - resume_path = Path(args.resume) - if not resume_path.exists(): - console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") - return - pop_X, _pop_F, resumed_gen, prev_history, ckpt_n_runs = load_checkpoint(resume_path) - if ckpt_n_runs != n_runs: - console.print(f"[bold red]Checkpoint n_runs={ckpt_n_runs} != --n-runs={n_runs}[/]") - return - problem.history = prev_history - console.print(f"[bold green]Resumed[/] from generation {resumed_gen} " - f"({len(prev_history)} prior evaluations)") - remaining = args.n_gen - resumed_gen - if remaining <= 0: - console.print(f"[bold yellow]Already completed {resumed_gen} generations " - f"(requested {args.n_gen}). Increase --n-gen to continue.[/]") - return - termination = get_termination("n_gen", remaining) - algorithm = NSGA2( - pop_size=len(pop_X), - sampling=pop_X, - crossover=SBX(prob=0.9, eta=15), - mutation=PM(eta=20), - eliminate_duplicates=True, - ) - else: - algorithm = NSGA2( - pop_size=args.pop_size, - sampling=LHS(), - crossover=SBX(prob=0.9, eta=15), - mutation=PM(eta=20), - eliminate_duplicates=True, - ) - termination = get_termination("n_gen", args.n_gen) - - if dashboard: - dashboard.start() - - try: - res = minimize( - problem, - algorithm, - termination, - seed=args.seed, - verbose=not use_dashboard, - callback=callback, - ) - except KeyboardInterrupt: - if dashboard: - dashboard.stop() - console.print("\n[bold yellow]Interrupted![/] Checkpoint was saved after last " - f"completed generation to:\n {checkpoint_path}") - console.print("Resume with: [bold]--resume " + str(checkpoint_path) + "[/]") - os._exit(1) # skip atexit handlers that hang on worker join - finally: - if dashboard: - dashboard.stop() - - # --- Results --- - console.print(f"\n[bold]Optimization complete.[/] " - f"{len(res.F)} Pareto-optimal solutions found.\n") - - pareto_configs = [] - for i, (x, f) in enumerate(zip(res.X, res.F)): - params = decode_ensemble_params(x, n_runs) - f1 = -f[0] - tc = -f[1] - wt = f[2] if args.optimize_time else None - pareto_configs.append({"params": params, "f1_cv": f1, "tc_cv": tc, "wall_time": wt}) - - table = Table(title="Pareto Front (sorted by CV F1)") - table.add_column("#", style="dim", width=3) - table.add_column("CV F1", justify="right") - table.add_column("CV TC", justify="right") - table.add_column("Parameters") - - sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) - for i, cfg in enumerate(sorted_pareto): - f1_style = "bold green" if cfg["f1_cv"] > baseline_cv["f1"] else "" - tc_style = "bold green" if cfg["tc_cv"] > baseline_cv["tc"] else "" - table.add_row( - str(i), - Text(f"{cfg['f1_cv']:.4f}", style=f1_style), - Text(f"{cfg['tc_cv']:.4f}", style=tc_style), - format_ensemble_short(cfg["params"]), - ) - console.print(table) - - # --- Re-evaluate best on FULL dataset --- - best_f1_idx = np.argmin(res.F[:, 0]) - best = pareto_configs[best_f1_idx] - - console.print(f"\n{'='*60}") - console.print(f"[bold]Best CV F1 solution:[/] CV F1={best['f1_cv']:.4f} CV TC={best['tc_cv']:.4f}") - console.print(format_ensemble_long(best["params"])) - - console.print(f"\n[bold]Full-dataset evaluation[/] (checking for overfit)") - best_full = evaluate_ensemble(best["params"], cases, args.n_threads) - console.print(f" Full F1=[bold]{best_full['f1']:.4f}[/] Full TC=[bold]{best_full['tc']:.4f}[/] " - f"Recall={best_full['recall']:.4f} Precision={best_full['precision']:.4f}") - for cat, v in sorted(best_full["per_category"].items()): - console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") - - gap_f1 = best_full["f1"] - best["f1_cv"] - gap_tc = best_full["tc"] - best["tc_cv"] - console.print(f"\n Overfit check (full - CV): F1 {gap_f1:+.4f} TC {gap_tc:+.4f}") - if gap_f1 > 0.02: - console.print(" [bold yellow]Warning:[/] Full-data F1 notably higher than CV — possible overfit") - else: - console.print(" [bold green]OK:[/] Full-data and CV scores are consistent") - - # --- Save --- - checkpoint = { - "pareto_configs": pareto_configs, - "history": problem.history, - "baseline_cv": baseline_cv, - "best_full": best_full, - "folds_info": [(len(tr), len(te)) for tr, te in folds], - "args": vars(args), - } - ckpt_path = output_dir / "optim_checkpoint.pkl" - with open(ckpt_path, "wb") as f: - pickle.dump(checkpoint, f) - console.print(f"\nCheckpoint saved to {ckpt_path}") - - summary_path = output_dir / "pareto_front.txt" - with open(summary_path, "w") as f: - f.write(f"# Pareto-optimal kalign ensemble configs (NSGA-II + {k}-fold stratified CV)\n") - f.write(f"# n_runs={n_runs} pop_size={args.pop_size} n_gen={args.n_gen} " - f"n_cases={len(cases)} seed={args.seed}\n") - f.write(f"# Baseline CV: F1={baseline_cv['f1']:.4f} TC={baseline_cv['tc']:.4f}\n\n") - for i, cfg in enumerate(sorted_pareto): - p = cfg["params"] - f.write(f"[{i}] CV_F1={cfg['f1_cv']:.4f} CV_TC={cfg['tc_cv']:.4f}\n") - for rk in range(n_runs): - mat = MATRIX_NAMES.get(p["run_types"][rk], "?") - f.write(f" Run {rk}: gpo={p['run_gpo'][rk]:.3f} gpe={p['run_gpe'][rk]:.3f} " - f"tgpe={p['run_tgpe'][rk]:.3f} noise={p['run_noise'][rk]:.3f} " - f"matrix={mat}\n") - f.write(f" Shared: vsm_amax={p['vsm_amax']:.3f} " - f"consistency={p['consistency']} " - f"consistency_weight={p['consistency_weight']:.3f} " - f"realign={p['realign']} min_support={p['min_support']}\n\n") - console.print(f"Pareto front saved to {summary_path}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/optimize_parallel.py b/benchmarks/optimize_parallel.py deleted file mode 100644 index 19540e8..0000000 --- a/benchmarks/optimize_parallel.py +++ /dev/null @@ -1,444 +0,0 @@ -"""Optuna optimization of threadpool parallelization parameters. - -Runs separate optimizations at each thread count (1, 2, 4, 8, 16, 32, 64) -to find optimal thresholds AND where thread scaling stops helping. - -Usage: - uv run python benchmarks/optimize_parallel.py --n-trials 100 --fresh - uv run python benchmarks/optimize_parallel.py --n-trials 5 --dry-run -""" - -import argparse -import faulthandler -import json -import os -import signal -import sys -import time -from pathlib import Path - -import optuna - -import kalign - -# Enable faulthandler so segfaults print a traceback -faulthandler.enable() -if hasattr(signal, "SIGUSR1"): - faulthandler.register(signal.SIGUSR1) - -# --------------------------------------------------------------------------- -# Dataset generation -# --------------------------------------------------------------------------- - -DSSIM_SPECS = [ - # (name, n_seq, length, dna, n_obs) - ("prot_200", 200, 300, False, 50), - ("prot_500", 500, 300, False, 50), - ("prot_1000", 1000, 200, False, 50), - ("dna_200", 200, 500, True, 50), - ("dna_500", 500, 1000, True, 50), - ("dna_1000", 1000, 500, True, 50), - ("dna_viral", 200, 5000, True, 30), - ("dna_viral_lg", 500, 3000, True, 30), -] - -THREAD_COUNTS = [1, 2, 4, 8, 16, 32, 64] - - -def generate_dssim_datasets(cache_dir: Path) -> list[tuple[str, list[str], str]]: - """Generate DSSim datasets, caching to disk.""" - datasets = [] - cache_dir.mkdir(parents=True, exist_ok=True) - - for name, n_seq, length, dna, n_obs in DSSIM_SPECS: - cache_file = cache_dir / f"{name}.fasta" - seq_type = "dna" if dna else "protein" - - if cache_file.exists(): - seqs = [] - current: list[str] = [] - for line in cache_file.read_text().splitlines(): - if line.startswith(">"): - if current: - seqs.append("".join(current)) - current = [] - else: - current.append(line.strip()) - if current: - seqs.append("".join(current)) - print(f" {name}: {len(seqs)} seqs (cached)") - else: - print(f" {name}: generating {n_seq} seqs, len={length}, " - f"{'DNA' if dna else 'protein'}...") - seqs = kalign.generate_test_sequences( - n_seq=n_seq, n_obs=n_obs, dna=dna, length=length, seed=42 - ) - with open(cache_file, "w") as f: - for i, s in enumerate(seqs): - f.write(f">seq{i}\n{s}\n") - print(f" {name}: {len(seqs)} seqs generated") - - datasets.append((name, seqs, seq_type)) - - return datasets - - -def load_balifam100() -> list[tuple[str, list[str], str]]: - """Load BaliFam100 unaligned sequences.""" - try: - try: - from benchmarks.datasets import BALIFAM_DIR, balifam_download - except ImportError: - from datasets import BALIFAM_DIR, balifam_download - - if not BALIFAM_DIR.exists() or not any(BALIFAM_DIR.iterdir()): - print(" Downloading BaliFam100...") - balifam_download() - - in_dir = BALIFAM_DIR / "balifam100" / "in" - if not in_dir.exists(): - in_dir = BALIFAM_DIR / "in" - if not in_dir.exists(): - print(" WARNING: BaliFam100 in/ not found, skipping") - return [] - - datasets = [] - for fasta in sorted(in_dir.glob("*")): - if not fasta.is_file(): - continue - seqs = [] - current: list[str] = [] - for line in fasta.read_text().splitlines(): - if line.startswith(">"): - if current: - seqs.append("".join(current)) - current = [] - else: - current.append(line.strip()) - if current: - seqs.append("".join(current)) - if len(seqs) >= 10: - datasets.append((f"bf100_{fasta.stem}", seqs, "protein")) - - print(f" BaliFam100: {len(datasets)} families loaded") - return datasets - - except Exception as e: - print(f" WARNING: Could not load BaliFam100: {e}") - return [] - - -# --------------------------------------------------------------------------- -# Benchmark runner -# --------------------------------------------------------------------------- - - -def time_alignment(seqs: list[str], seq_type: str, mode: str) -> float: - start = time.perf_counter() - kalign.align(seqs, seq_type=seq_type, mode=mode) - return time.perf_counter() - start - - -def run_benchmark_suite( - datasets: list[tuple[str, list[str], str]], - mode: str, - trial: optuna.trial.Trial | None = None, -) -> float: - total = 0.0 - for i, (_name, seqs, seq_type) in enumerate(datasets): - t = time_alignment(seqs, seq_type, mode) - total += t - if trial is not None: - trial.report(total, i) - if trial.should_prune(): - raise optuna.TrialPruned() - return total - - -# --------------------------------------------------------------------------- -# Per-thread-count optimization -# --------------------------------------------------------------------------- - - -def optimize_for_thread_count( - nt: int, - datasets: list[tuple[str, list[str], str]], - mode: str, - n_trials: int, - db_path: Path, -) -> dict: - """Run Optuna optimization at a fixed thread count. Returns best result dict.""" - - n_repeats = 3 - - def objective(trial: optuna.trial.Trial) -> float: - aln_serial = trial.suggest_int("aln_serial_threshold", 50, 1000, step=10) - kmeans_upgma = trial.suggest_int("kmeans_upgma_threshold", 10, 150, step=5) - dist_min = trial.suggest_int("dist_min_seqs", 0, 100, step=5) - pfor_chunk = trial.suggest_int("pfor_min_chunk", 1, 32) - - sys.stderr.write( - f"[{nt}T trial {trial.number}] aln={aln_serial} km={kmeans_upgma} " - f"dist={dist_min} pfor={pfor_chunk}\n" - ) - sys.stderr.flush() - - kalign.set_parallel_config( - aln_serial_threshold=aln_serial, - kmeans_upgma_threshold=kmeans_upgma, - dist_min_seqs=dist_min, - pfor_min_chunk=pfor_chunk, - ) - kalign.set_num_threads(nt) - - try: - times = [] - for r in range(n_repeats): - t = run_benchmark_suite(datasets, mode, trial if r == 0 else None) - times.append(t) - times.sort() - return times[len(times) // 2] - except Exception: - return float("inf") - - optuna.logging.set_verbosity(optuna.logging.WARNING) - - storage = f"sqlite:///{db_path}" - study_name = f"parallel_opt_{mode}_{nt}t" - - study = optuna.create_study( - study_name=study_name, - storage=storage, - load_if_exists=True, - direction="minimize", - sampler=optuna.samplers.TPESampler(seed=42), - pruner=optuna.pruners.MedianPruner( - n_startup_trials=5, - n_warmup_steps=len(datasets) // 3, - ), - ) - - existing = len(study.trials) - remaining = max(0, n_trials - existing) - - if existing > 0 and remaining > 0: - print(f" Resuming ({existing} done, {remaining} remaining)") - - if remaining <= 0: - print(f" Already complete ({existing} trials)") - else: - t_start = time.perf_counter() - n_done = [0] - - def callback(study: optuna.study.Study, trial: optuna.trial.FrozenTrial): - n_done[0] += 1 - if trial.state == optuna.trial.TrialState.COMPLETE: - best = study.best_trial - is_new = trial.number == best.number - elapsed = time.perf_counter() - t_start - p = trial.params - bp = best.params - print( - f" [{n_done[0]:>3}/{remaining}] " - f"{trial.value:>7.1f}s " - f"aln={p['aln_serial_threshold']:<4} " - f"km={p['kmeans_upgma_threshold']:<4} " - f"dist={p['dist_min_seqs']:<4} " - f"pfor={p['pfor_min_chunk']:<3} " - f"| best={best.value:.1f}s " - f"(aln={bp['aln_serial_threshold']} km={bp['kmeans_upgma_threshold']} " - f"dist={bp['dist_min_seqs']} pfor={bp['pfor_min_chunk']}) " - f"({elapsed:.0f}s)" - f"{' ***' if is_new else ''}" - ) - elif trial.state == optuna.trial.TrialState.PRUNED: - print(f" [{n_done[0]:>3}/{remaining}] pruned") - - study.optimize(objective, n_trials=remaining, callbacks=[callback]) - - best = study.best_trial - return { - "n_threads": nt, - "best_time": best.value, - "params": best.params, - "trial_number": best.number, - } - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - - -def main(): - parser = argparse.ArgumentParser( - description="Optimize threadpool parameters per thread count" - ) - parser.add_argument( - "--n-trials", type=int, default=100, - help="Optuna trials per thread count (default: 100)" - ) - parser.add_argument( - "--mode", default="fast", - choices=["fast", "default", "accurate"], - help="Kalign mode preset (default: fast)" - ) - parser.add_argument( - "--no-balifam", action="store_true", - help="Skip BaliFam100 datasets" - ) - parser.add_argument( - "--dry-run", action="store_true", - help="Just run baselines at each thread count, don't optimize" - ) - parser.add_argument( - "--cache-dir", type=str, - default=str(Path(__file__).parent / "data" / "parallel_opt"), - help="Directory for cached datasets and study DB" - ) - parser.add_argument( - "--fresh", action="store_true", - help="Delete previous studies and start fresh" - ) - parser.add_argument( - "--thread-counts", type=str, default=None, - help="Comma-separated thread counts (default: 1,2,4,8,16,32,64)" - ) - args = parser.parse_args() - - if args.thread_counts: - thread_counts = [int(x) for x in args.thread_counts.split(",")] - else: - max_cpu = os.cpu_count() or 64 - thread_counts = [t for t in THREAD_COUNTS if t <= max_cpu] - - cache_dir = Path(args.cache_dir) - db_path = cache_dir / "parallel_opt.db" - - print(f"{'='*72}") - print(f" Threadpool Parameter Optimization") - print(f" Mode: {args.mode} | Trials/thread-count: {args.n_trials}") - print(f" Thread counts: {thread_counts}") - print(f"{'='*72}") - print() - - # Generate/load datasets - print("Preparing datasets...") - datasets = generate_dssim_datasets(cache_dir) - - if not args.no_balifam: - bf = load_balifam100() - datasets.extend(bf) - - print(f"\nTotal: {len(datasets)} benchmark cases") - print() - - # Warm up - print("Warming up...") - warmup_seqs = kalign.generate_test_sequences( - n_seq=50, n_obs=20, dna=False, length=100, seed=99 - ) - kalign.align(warmup_seqs, seq_type="protein", mode=args.mode) - print() - - # Baseline at each thread count - print("Running baselines (default params at each thread count)...") - baselines = {} - kalign.set_parallel_config() - for nt in thread_counts: - kalign.set_num_threads(nt) - t = run_benchmark_suite(datasets, args.mode) - baselines[nt] = t - print(f" {nt:>2} threads: {t:.2f}s") - print() - - if args.dry_run: - print("Dry run complete.") - return - - # Fresh start - if args.fresh and db_path.exists(): - print(f"--fresh: deleting {db_path}") - db_path.unlink() - print() - - # Optimize at each thread count - results = [] - for nt in thread_counts: - print(f"--- Optimizing for {nt} threads ({args.n_trials} trials) ---") - result = optimize_for_thread_count( - nt, datasets, args.mode, args.n_trials, db_path - ) - result["baseline_time"] = baselines[nt] - results.append(result) - print() - - # Summary table - print() - print(f"{'='*90}") - print(f" SCALING & OPTIMIZATION RESULTS") - print(f"{'='*90}") - print() - print(f" {'Threads':>7} {'Baseline':>10} {'Optimized':>10} {'Speedup':>8} " - f"{'aln_ser':>8} {'km_upgma':>8} {'dist_min':>8} {'pfor_ch':>8}") - print(f" {'-'*7} {'-'*10} {'-'*10} {'-'*8} " - f"{'-'*8} {'-'*8} {'-'*8} {'-'*8}") - - best_overall = None - for r in results: - nt = r["n_threads"] - bl = r["baseline_time"] - opt = r["best_time"] - speedup = bl / opt if opt > 0 else 0 - p = r["params"] - is_best = best_overall is None or opt < best_overall["best_time"] - if is_best: - best_overall = r - marker = " <-- fastest" if is_best else "" - print( - f" {nt:>7} {bl:>9.2f}s {opt:>9.2f}s {speedup:>7.2f}x " - f"{p['aln_serial_threshold']:>8} " - f"{p['kmeans_upgma_threshold']:>8} " - f"{p['dist_min_seqs']:>8} " - f"{p['pfor_min_chunk']:>8}" - f"{marker}" - ) - - print() - print(f" Fastest overall: {best_overall['n_threads']} threads, " - f"{best_overall['best_time']:.2f}s") - print() - - # Scaling analysis - single = None - for r in results: - if r["n_threads"] == 1: - single = r["best_time"] - break - if single: - print(" Thread scaling (optimized):") - for r in results: - par_speedup = single / r["best_time"] if r["best_time"] > 0 else 0 - efficiency = par_speedup / r["n_threads"] * 100 if r["n_threads"] > 0 else 0 - bar = "#" * int(par_speedup * 2) - print(f" {r['n_threads']:>3}T: {par_speedup:>5.1f}x " - f"({efficiency:>4.0f}% eff) {bar}") - print() - - # Save results - results_dir = Path(__file__).parent / "results" - results_dir.mkdir(exist_ok=True) - out = { - "mode": args.mode, - "n_trials_per_thread_count": args.n_trials, - "n_datasets": len(datasets), - "results": results, - } - out_file = results_dir / "parallel_opt.json" - with open(out_file, "w") as f: - json.dump(out, f, indent=2) - print(f"Results saved to {out_file}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/optimize_params.py b/benchmarks/optimize_params.py deleted file mode 100644 index 9c273df..0000000 --- a/benchmarks/optimize_params.py +++ /dev/null @@ -1,1181 +0,0 @@ -#!/usr/bin/env python3 -"""Multi-objective hyperparameter optimization for kalign using pymoo. - -Uses stratified k-fold cross-validation so NSGA-II optimises on held-out -scores, not training scores. Each evaluation splits BAliBASE families -into k folds (stratified by RV category), aligns k-1 folds, and scores -on the held-out fold. The objective is the mean held-out F1 / TC. - -Objectives (maximized): - 1. Mean held-out F1 across folds (category-averaged within each fold) - 2. Mean held-out TC across folds (category-averaged within each fold) - -Decision variables: - - gpo: gap open penalty [2.0, 15.0] - - gpe: gap extend penalty [0.5, 5.0] - - tgpe: terminal gap extend [0.1, 3.0] - - vsm_amax: variable scoring matrix [0.0, 5.0] - - seq_weights: profile rebalancing [0.0, 5.0] - - consistency: anchor consistency rounds {0, 1, 2, 3, 5, 8} - - consistency_weight: consistency bonus weight [0.5, 5.0] - - realign: tree-rebuild iterations {0, 1, 2} - - matrix: substitution matrix choice {PFASUM43, PFASUM60, CorBLOSUM66} - -Usage: - # Quick test (small pop, few generations) - uv run python -m benchmarks.optimize_params --pop-size 20 --n-gen 10 - - # Full optimization (5-fold CV, all 218 cases) - uv run python -m benchmarks.optimize_params --pop-size 60 --n-gen 80 --n-threads 4 - - # Faster: 3-fold CV - uv run python -m benchmarks.optimize_params --n-folds 3 --pop-size 40 --n-gen 50 - - # Add wall time as 3rd objective - uv run python -m benchmarks.optimize_params --optimize-time -""" - -import argparse -import os -import pickle -import signal -import sys -import tempfile -import time -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -import numpy as np - -try: - from pymoo.algorithms.moo.nsga2 import NSGA2 # type: ignore[import-untyped] - from pymoo.core.callback import Callback # type: ignore[import-untyped] - from pymoo.core.problem import Problem # type: ignore[import-untyped] - from pymoo.operators.crossover.sbx import SBX # type: ignore[import-untyped] - from pymoo.operators.mutation.pm import PM # type: ignore[import-untyped] - from pymoo.operators.sampling.lhs import LHS # type: ignore[import-untyped] - from pymoo.optimize import minimize # type: ignore[import-untyped] - from pymoo.termination import get_termination # type: ignore[import-untyped] -except ImportError: - print("pymoo not installed. Run: uv pip install pymoo") - sys.exit(1) - -from rich.console import Console # type: ignore[import-untyped] -from rich.layout import Layout # type: ignore[import-untyped] -from rich.live import Live # type: ignore[import-untyped] -from rich.panel import Panel # type: ignore[import-untyped] -from rich.progress import ( # type: ignore[import-untyped] - BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, -) -from rich.table import Table # type: ignore[import-untyped] -from rich.text import Text # type: ignore[import-untyped] - -import kalign # type: ignore[import-untyped] - -from .datasets import BenchmarkCase, balibase_cases, balibase_download, balibase_is_available -from .scoring import score_alignment_detailed - -# --------------------------------------------------------------------------- -# Parameter space definition -# --------------------------------------------------------------------------- - -# Continuous variables: [gpo, gpe, tgpe, vsm_amax, seq_weights, consistency_weight] -CONT_LOWER = np.array([2.0, 0.5, 0.1, 0.0, 0.0, 0.5]) -CONT_UPPER = np.array([15.0, 5.0, 3.0, 5.0, 5.0, 5.0]) -CONT_NAMES = ["gpo", "gpe", "tgpe", "vsm_amax", "seq_weights", "consistency_weight"] - -# Integer/categorical variables: [consistency, realign, matrix] -INT_LOWER = np.array([0, 0, 0]) -INT_UPPER = np.array([6, 2, 2]) # consistency: 0-6 maps via table, realign: 0-2, matrix: 0-2 -INT_NAMES = ["consistency", "realign", "matrix"] - -CONSISTENCY_MAP = [0, 1, 2, 3, 5, 8, 10] -MATRIX_MAP = ["pfasum43", "pfasum60", "protein"] # API values for kalign -MATRIX_DISPLAY = {"pfasum43": "PFASUM43", "pfasum60": "PFASUM60", "protein": "CorBLOSUM66"} - -N_CONT = len(CONT_LOWER) -N_INT = len(INT_LOWER) -N_VAR = N_CONT + N_INT - - -def decode_params(x): - """Decode a parameter vector into a dict of kalign parameters.""" - cont = x[:N_CONT] - ints = np.round(x[N_CONT:]).astype(int) - - consistency_idx = int(np.clip(ints[0], 0, len(CONSISTENCY_MAP) - 1)) - realign_idx = int(np.clip(ints[1], 0, 2)) - matrix_idx = int(np.clip(ints[2], 0, len(MATRIX_MAP) - 1)) - - return { - "gap_open": float(cont[0]), - "gap_extend": float(cont[1]), - "terminal_gap_extend": float(cont[2]), - "vsm_amax": float(cont[3]), - "seq_weights": float(cont[4]), - "consistency_weight": float(cont[5]), - "consistency": CONSISTENCY_MAP[consistency_idx], - "realign": realign_idx, - "seq_type": MATRIX_MAP[matrix_idx], - } - - -def encode_params(params): - """Encode a params dict back into a decision vector (inverse of decode_params).""" - cont = np.array([ - params["gap_open"], params["gap_extend"], params["terminal_gap_extend"], - params["vsm_amax"], params["seq_weights"], params["consistency_weight"], - ]) - consistency_idx = CONSISTENCY_MAP.index(params["consistency"]) - realign_idx = params["realign"] - matrix_idx = MATRIX_MAP.index(params["seq_type"]) - ints = np.array([consistency_idx, realign_idx, matrix_idx], dtype=float) - return np.concatenate([cont, ints]) - - -def _matrix_name(params): - """Human-readable matrix name from seq_type API value.""" - return MATRIX_DISPLAY.get(params["seq_type"], params["seq_type"]) - - -def format_params_short(params): - """Compact one-line summary of a parameter dict.""" - return (f"gpo={params['gap_open']:.1f} gpe={params['gap_extend']:.2f} " - f"tgpe={params['terminal_gap_extend']:.2f} vsm={params['vsm_amax']:.1f} " - f"sw={params['seq_weights']:.1f} c={params['consistency']} " - f"cw={params['consistency_weight']:.1f} re={params['realign']} " - f"{_matrix_name(params)}") - - -def format_params_long(params): - """Verbose one-line summary.""" - return (f"gpo={params['gap_open']:.2f} gpe={params['gap_extend']:.2f} " - f"tgpe={params['terminal_gap_extend']:.2f} vsm={params['vsm_amax']:.2f} " - f"sw={params['seq_weights']:.2f} cons={params['consistency']} " - f"cw={params['consistency_weight']:.2f} re={params['realign']} " - f"mat={_matrix_name(params)}") - - -# --------------------------------------------------------------------------- -# Stratified k-fold CV -# --------------------------------------------------------------------------- - -def stratified_kfold(cases: List[BenchmarkCase], k: int, seed: int = 42 - ) -> List[Tuple[List[BenchmarkCase], List[BenchmarkCase]]]: - """Split cases into k stratified folds by dataset (RV category). - - Returns list of (train, test) pairs. Each fold's test set contains - roughly equal representation from every RV category. - """ - rng = np.random.RandomState(seed) - - # Group by category - by_cat: Dict[str, List[BenchmarkCase]] = defaultdict(list) - for c in cases: - by_cat[c.dataset].append(c) - - # Shuffle within each category and assign fold indices - fold_assignments: Dict[str, int] = {} - for cat_cases in by_cat.values(): - indices = list(range(len(cat_cases))) - rng.shuffle(indices) - for rank, idx in enumerate(indices): - fold_assignments[cat_cases[idx].family] = rank % k - - # Build folds - folds = [] - for fold_idx in range(k): - test = [c for c in cases if fold_assignments[c.family] == fold_idx] - train = [c for c in cases if fold_assignments[c.family] != fold_idx] - folds.append((train, test)) - - return folds - - -# --------------------------------------------------------------------------- -# Evaluation -# --------------------------------------------------------------------------- - -def evaluate_params(params, cases, n_threads=1, quiet=True): - """Run kalign with given params on all cases, return mean metrics. - - Returns dict with keys: f1, tc, recall, precision, wall_time, per_category. - """ - results_by_cat: Dict[str, list] = {} - total_time = 0.0 - - for case in cases: - with tempfile.TemporaryDirectory() as tmpdir: - output = Path(tmpdir) / f"{case.family}_aln.fasta" - - try: - start = time.perf_counter() - kalign.align_file_to_file( - str(case.unaligned), - str(output), - format="fasta", - seq_type=params["seq_type"], - gap_open=params["gap_open"], - gap_extend=params["gap_extend"], - terminal_gap_extend=params["terminal_gap_extend"], - n_threads=n_threads, - vsm_amax=params["vsm_amax"], - seq_weights=params["seq_weights"], - consistency=params["consistency"], - consistency_weight=params["consistency_weight"], - realign=params["realign"], - ) - wall_time = time.perf_counter() - start - total_time += wall_time - - detailed = score_alignment_detailed(case.reference, output) - - cat = case.dataset - if cat not in results_by_cat: - results_by_cat[cat] = [] - results_by_cat[cat].append(detailed) - - except Exception as e: - if not quiet: - print(f" WARN: {case.family}: {e}", file=sys.stderr) - - if not results_by_cat: - return {"f1": 0.0, "tc": 0.0, "recall": 0.0, "precision": 0.0, - "wall_time": total_time, "per_category": {}} - - # Compute per-category means - per_cat = {} - for cat, scores in results_by_cat.items(): - per_cat[cat] = { - "f1": np.mean([s["f1"] for s in scores]), - "tc": np.mean([s["tc"] for s in scores]), - "recall": np.mean([s["recall"] for s in scores]), - "precision": np.mean([s["precision"] for s in scores]), - "n": len(scores), - } - - # Overall means (category-averaged so small categories count equally) - all_f1 = [v["f1"] for v in per_cat.values()] - all_tc = [v["tc"] for v in per_cat.values()] - all_recall = [v["recall"] for v in per_cat.values()] - all_precision = [v["precision"] for v in per_cat.values()] - - return { - "f1": float(np.mean(all_f1)), - "tc": float(np.mean(all_tc)), - "recall": float(np.mean(all_recall)), - "precision": float(np.mean(all_precision)), - "wall_time": total_time, - "per_category": per_cat, - } - - -def evaluate_cv(params, folds, n_threads=1, quiet=True): - """Evaluate params using stratified k-fold CV. - - For each fold, aligns the test cases and scores them. - Returns the mean held-out F1 and TC across folds, - plus per-fold details and total wall time. - """ - fold_f1s = [] - fold_tcs = [] - total_time = 0.0 - - for _, test in folds: - result = evaluate_params(params, test, n_threads, quiet) - fold_f1s.append(result["f1"]) - fold_tcs.append(result["tc"]) - total_time += result["wall_time"] - - return { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, - "fold_tcs": fold_tcs, - "wall_time": total_time, - } - - -# --------------------------------------------------------------------------- -# Rich live dashboard -# --------------------------------------------------------------------------- - -class Dashboard: - """Rich-based live terminal dashboard for optimization progress.""" - - def __init__(self, n_gen: int, pop_size: int, baseline_f1: float, baseline_tc: float, - optimize_time: bool = False, baseline_time: float = 0.0): - self.n_gen = n_gen - self.pop_size = pop_size - self.baseline_f1 = baseline_f1 - self.baseline_tc = baseline_tc - self.optimize_time = optimize_time - self.baseline_time = baseline_time - self.console = Console() - - # State - self.current_gen = 0 - self.eval_count = 0 - self.total_evals = n_gen * pop_size - self.gen_start_time = time.time() - self.run_start_time = time.time() - self.current_eval_params: Optional[dict] = None - self.current_eval_idx = 0 # within generation - - # Best-ever tracking - self.best_f1 = 0.0 - self.best_f1_params: Optional[dict] = None - self.best_tc = 0.0 - self.best_tc_params: Optional[dict] = None - self.best_time = float("inf") - self.best_time_params: Optional[dict] = None - - # Pareto front (updated per generation) - self.pareto_front: List[dict] = [] - - # Generation history (best F1 per gen for trend) - self.gen_history: List[dict] = [] - - # Recent evaluations (ring buffer for last 5) - self.recent_evals: List[dict] = [] - - # Progress bar - self.progress = Progress( - SpinnerColumn(), - TextColumn("[bold blue]{task.description}"), - BarColumn(bar_width=40), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeElapsedColumn(), - ) - self.gen_task = self.progress.add_task("Generation", total=n_gen) - self.eval_task = self.progress.add_task(" Eval", total=pop_size) - - self.live = Live(self._build_layout(), console=self.console, refresh_per_second=2) - - def start(self): - self.run_start_time = time.time() - self.live.start() - - def stop(self): - self.live.stop() - - def _delta_str(self, value: float, baseline: float) -> str: - """Format a value with delta vs baseline, colored.""" - delta = value - baseline - if delta > 0.005: - return f"[bold green]{value:.4f} (+{delta:.4f})[/]" - elif delta < -0.005: - return f"[bold red]{value:.4f} ({delta:.4f})[/]" - else: - return f"[dim]{value:.4f} ({delta:+.4f})[/]" - - def _build_status_panel(self) -> Panel: - elapsed = time.time() - self.run_start_time - gen_elapsed = time.time() - self.gen_start_time - - if self.eval_count > 0: - avg_per_eval = elapsed / self.eval_count - remaining = avg_per_eval * (self.total_evals - self.eval_count) - eta_str = f"{remaining / 60:.0f}m" if remaining > 60 else f"{remaining:.0f}s" - else: - eta_str = "..." - - lines = [ - f"Gen [bold]{self.current_gen}[/]/{self.n_gen} " - f"Eval [bold]{self.eval_count}[/]/{self.total_evals} " - f"Elapsed [bold]{elapsed / 60:.1f}m[/] " - f"ETA [bold]{eta_str}[/] " - f"Gen time [bold]{gen_elapsed:.1f}s[/]", - ] - - if self.current_eval_params: - lines.append(f"[dim]Current: {format_params_short(self.current_eval_params)}[/]") - - return Panel("\n".join(lines), title="Progress", border_style="blue") - - def _build_best_panel(self) -> Panel: - lines = [] - baseline_str = f"[dim]Baseline: F1={self.baseline_f1:.4f} TC={self.baseline_tc:.4f}" - if self.optimize_time: - baseline_str += f" Time={self.baseline_time:.1f}s" - baseline_str += "[/]" - lines.append(baseline_str) - lines.append("") - - if self.best_f1_params: - lines.append(f"Best F1: {self._delta_str(self.best_f1, self.baseline_f1)}") - lines.append(f" [dim]{format_params_short(self.best_f1_params)}[/]") - else: - lines.append("[dim]Best F1: (no evaluations yet)[/]") - - lines.append("") - - if self.best_tc_params: - lines.append(f"Best TC: {self._delta_str(self.best_tc, self.baseline_tc)}") - lines.append(f" [dim]{format_params_short(self.best_tc_params)}[/]") - else: - lines.append("[dim]Best TC: (no evaluations yet)[/]") - - if self.optimize_time: - lines.append("") - if self.best_time_params: - delta = self.best_time - self.baseline_time - if delta < -1.0: - time_str = f"[bold green]{self.best_time:.1f}s ({delta:.1f}s)[/]" - elif delta > 1.0: - time_str = f"[bold red]{self.best_time:.1f}s (+{delta:.1f}s)[/]" - else: - time_str = f"[dim]{self.best_time:.1f}s ({delta:+.1f}s)[/]" - lines.append(f"Fastest: {time_str}") - lines.append(f" [dim]{format_params_short(self.best_time_params)}[/]") - - return Panel("\n".join(lines), title="Best Solutions (CV)", border_style="green") - - def _build_pareto_table(self) -> Panel: - table = Table(title="Pareto Front", expand=True, show_lines=False, padding=(0, 1)) - table.add_column("#", style="dim", width=3) - table.add_column("CV F1", justify="right", width=8) - table.add_column("CV TC", justify="right", width=8) - if self.optimize_time: - table.add_column("Time", justify="right", width=7) - table.add_column("gpo", justify="right", width=5) - table.add_column("gpe", justify="right", width=5) - table.add_column("tgpe", justify="right", width=5) - table.add_column("vsm", justify="right", width=4) - table.add_column("sw", justify="right", width=4) - table.add_column("c", justify="right", width=2) - table.add_column("cw", justify="right", width=4) - table.add_column("re", justify="right", width=2) - table.add_column("mat", width=8) - - n_cols = 13 if self.optimize_time else 12 - - # Sort by F1 descending - sorted_front = sorted(self.pareto_front, key=lambda x: -x["f1"]) - - for i, sol in enumerate(sorted_front[:10]): # show top 10 - p = sol["params"] - f1_style = "bold green" if sol["f1"] > self.baseline_f1 else "" - tc_style = "bold green" if sol["tc"] > self.baseline_tc else "" - row = [ - str(i), - Text(f"{sol['f1']:.4f}", style=f1_style), - Text(f"{sol['tc']:.4f}", style=tc_style), - ] - if self.optimize_time: - wt = sol.get("wall_time", 0.0) - row.append(f"{wt:.1f}s" if wt else "?") - row.extend([ - f"{p['gap_open']:.1f}", - f"{p['gap_extend']:.2f}", - f"{p['terminal_gap_extend']:.2f}", - f"{p['vsm_amax']:.1f}", - f"{p['seq_weights']:.1f}", - str(p['consistency']), - f"{p['consistency_weight']:.1f}", - str(p['realign']), - _matrix_name(p), - ]) - table.add_row(*row) - - if not self.pareto_front: - table.add_row(*[""] * n_cols) - - return Panel(table, border_style="cyan") - - def _build_trend_panel(self) -> Panel: - if not self.gen_history: - return Panel("[dim]No generation data yet[/]", title="Trend", border_style="yellow") - - lines = [] - # Show sparkline-style trend using simple chars - bar_width = 30 - if len(self.gen_history) > 1: - f1_vals = [g["best_f1"] for g in self.gen_history] - lo = min(min(f1_vals), self.baseline_f1) - 0.01 - hi = max(max(f1_vals), self.baseline_f1) + 0.01 - rng = hi - lo if hi > lo else 1.0 - - # Baseline marker position - bl_pos = int((self.baseline_f1 - lo) / rng * bar_width) - - lines.append("[bold]Best CV F1 per generation:[/]") - for g in self.gen_history[-12:]: # last 12 gens - pos = int((g["best_f1"] - lo) / rng * bar_width) - bar = list("·" * bar_width) - bar[min(bl_pos, bar_width - 1)] = "│" # baseline marker - for j in range(min(pos, bar_width)): - bar[j] = "█" - bar_str = "".join(bar) - delta = g["best_f1"] - self.baseline_f1 - color = "green" if delta > 0 else "red" if delta < -0.005 else "dim" - lines.append(f" Gen {g['gen']:>3d} [{color}]{bar_str} {g['best_f1']:.4f}[/]") - - lines.append(f" [dim]│ = baseline ({self.baseline_f1:.4f})[/]") - else: - g = self.gen_history[0] - lines.append(f"Gen {g['gen']}: F1={g['best_f1']:.4f} TC={g['best_tc']:.4f}") - - return Panel("\n".join(lines), title="Trend", border_style="yellow") - - def _build_recent_panel(self) -> Panel: - if not self.recent_evals: - return Panel("[dim]No evaluations yet[/]", title="Recent", border_style="dim") - - lines = [] - for ev in self.recent_evals[-5:]: - f1 = ev["f1"] - tc = ev["tc"] - delta = f1 - self.baseline_f1 - color = "green" if delta > 0 else "red" if delta < -0.005 else "dim" - time_str = f" t={ev['wall_time']:.1f}s" if self.optimize_time else "" - lines.append( - f" [{color}]F1={f1:.4f} TC={tc:.4f}{time_str}[/] " - f"[dim]{format_params_short(ev['params'])}[/]" - ) - return Panel("\n".join(lines), title="Recent Evaluations", border_style="dim") - - def _build_layout(self): - layout = Layout() - layout.split_column( - Layout(self.progress, name="progress", size=3), - Layout(name="top", size=7), - Layout(name="middle"), - Layout(self._build_recent_panel(), name="bottom", size=8), - ) - layout["top"].split_row( - Layout(self._build_status_panel(), name="status"), - Layout(self._build_best_panel(), name="best"), - ) - layout["middle"].split_row( - Layout(self._build_pareto_table(), name="pareto", ratio=3), - Layout(self._build_trend_panel(), name="trend", ratio=2), - ) - return layout - - def refresh(self): - self.live.update(self._build_layout()) - - def on_eval_start(self, params: dict, eval_num: int, eval_in_gen: int): - self.eval_count = eval_num - self.current_eval_idx = eval_in_gen - self.current_eval_params = params - self.progress.update(self.eval_task, completed=eval_in_gen) - self.refresh() - - def on_eval_end(self, params: dict, cv_result: dict): - f1 = cv_result["f1"] - tc = cv_result["tc"] - wt = cv_result.get("wall_time", 0.0) - - self.recent_evals.append({"params": params, "f1": f1, "tc": tc, "wall_time": wt}) - if len(self.recent_evals) > 5: - self.recent_evals.pop(0) - - if f1 > self.best_f1: - self.best_f1 = f1 - self.best_f1_params = params - if tc > self.best_tc: - self.best_tc = tc - self.best_tc_params = params - if self.optimize_time and wt < self.best_time and f1 > 0.5: - self.best_time = wt - self.best_time_params = params - - self.refresh() - - def on_gen_start(self, gen: int): - self.current_gen = gen - self.gen_start_time = time.time() - self.progress.update(self.gen_task, completed=gen) - self.progress.update(self.eval_task, completed=0) - self.refresh() - - def on_gen_end(self, gen: int, pareto_front: List[dict]): - self.pareto_front = pareto_front - - best_f1_in_gen = max((s["f1"] for s in pareto_front), default=0.0) - best_tc_in_gen = max((s["tc"] for s in pareto_front), default=0.0) - self.gen_history.append({ - "gen": gen, - "best_f1": best_f1_in_gen, - "best_tc": best_tc_in_gen, - "n_pareto": len(pareto_front), - }) - - self.progress.update(self.gen_task, completed=gen) - self.refresh() - - -# --------------------------------------------------------------------------- -# Parallel evaluation helper (must be top-level for pickling) -# --------------------------------------------------------------------------- - -def _eval_one(args_tuple): - """Evaluate one parameter vector. Top-level function for ProcessPoolExecutor.""" - x, folds, n_threads = args_tuple - params = decode_params(x) - cv_result = evaluate_cv(params, folds, n_threads, quiet=True) - return params, cv_result - - -def _eval_one_fold(args_tuple): - """Evaluate one (individual, fold) pair. Finer-grained parallelism.""" - ind_idx, fold_idx, x, test_cases, n_threads = args_tuple - params = decode_params(x) - result = evaluate_params(params, test_cases, n_threads, quiet=True) - return ind_idx, fold_idx, params, result - - -def _kill_pool(pool: ProcessPoolExecutor) -> None: - """Forcefully terminate all worker processes in the pool.""" - # Access the internal process map to send SIGTERM to each worker - for pid in list(pool._processes): # noqa: SLF001 - try: - os.kill(pid, signal.SIGTERM) - except OSError: - pass - pool.shutdown(wait=False, cancel_futures=True) - - -# --------------------------------------------------------------------------- -# pymoo Problem + Callback -# --------------------------------------------------------------------------- - -class KalignCVProblem(Problem): - """Multi-objective optimization with stratified CV evaluation.""" - - def __init__(self, folds, n_threads=1, n_workers=1, optimize_time=False, - dashboard: Optional[Dashboard] = None): - n_obj = 3 if optimize_time else 2 - xl = np.concatenate([CONT_LOWER, INT_LOWER.astype(float)]) - xu = np.concatenate([CONT_UPPER, INT_UPPER.astype(float)]) - - super().__init__( - n_var=N_VAR, - n_obj=n_obj, - xl=xl, - xu=xu, - ) - self.folds = folds - self.n_threads = n_threads - self.n_workers = n_workers - self.optimize_time = optimize_time - self.dashboard = dashboard - self.eval_count = 0 - self.history: List[dict] = [] - - def _evaluate(self, X, out, *args, **kwargs): # pyright: ignore[reportUnusedVariable] - F = np.zeros((X.shape[0], self.n_obj)) - - if self.n_workers > 1: - self._evaluate_parallel(X, F) - else: - self._evaluate_serial(X, F) - - out["F"] = F - - def _evaluate_serial(self, X, F): - for i, x in enumerate(X): - params = decode_params(x) - self.eval_count += 1 - - if self.dashboard: - self.dashboard.on_eval_start(params, self.eval_count, i) - - cv_result = evaluate_cv(params, self.folds, self.n_threads, quiet=True) - self._record(i, F, params, cv_result) - - def _evaluate_parallel(self, X, F): - """Fine-grained parallelism: submit (individual × fold) jobs. - - For pop_size=60, k=5 folds → 300 jobs, much better load balancing - than 60 coarse-grained jobs where slow individuals block the generation. - """ - n_folds = len(self.folds) - n_pop = X.shape[0] - - # Build flat job list: (ind_idx, fold_idx, x, test_cases, n_threads) - jobs = [] - for i, x in enumerate(X): - for fold_idx, (_, test) in enumerate(self.folds): - jobs.append((i, fold_idx, x, test, self.n_threads)) - - if self.dashboard: - self.dashboard.on_eval_start( - {"gap_open": 0, "gap_extend": 0, "terminal_gap_extend": 0, - "vsm_amax": 0, "seq_weights": 0, "consistency_weight": 0, - "consistency": 0, "realign": 0, "seq_type": "..."}, - self.eval_count + 1, 0) - - # Collect per-fold results: fold_results[ind_idx] = {fold_idx: result} - fold_results: Dict[int, Dict[int, dict]] = defaultdict(dict) - ind_params: Dict[int, dict] = {} - - pool = ProcessPoolExecutor(max_workers=self.n_workers) - try: - futures = {pool.submit(_eval_one_fold, j): j[:2] for j in jobs} - completed_individuals = set() - completed_jobs = 0 - - for future in as_completed(futures): - ind_idx, fold_idx, params, result = future.result() - fold_results[ind_idx][fold_idx] = result - ind_params[ind_idx] = params - completed_jobs += 1 - - # Check if this individual now has all folds complete - if len(fold_results[ind_idx]) == n_folds: - completed_individuals.add(ind_idx) - self.eval_count += 1 - - # Aggregate fold results into CV score - fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] - fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] - total_time = sum(fold_results[ind_idx][fi]["wall_time"] for fi in range(n_folds)) - - cv_result = { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, - "fold_tcs": fold_tcs, - "wall_time": total_time, - } - - self._record(ind_idx, F, params, cv_result) - - if self.dashboard: - self.dashboard.on_eval_start( - params, self.eval_count, len(completed_individuals)) - - except KeyboardInterrupt: - _kill_pool(pool) - raise - finally: - pool.shutdown(wait=False) - - def _record(self, i, F, params, cv_result): - F[i, 0] = -cv_result["f1"] - F[i, 1] = -cv_result["tc"] - if self.optimize_time: - F[i, 2] = cv_result["wall_time"] - - self.history.append({ - "eval": self.eval_count, - "params": params, - "cv_result": cv_result, - }) - - if self.dashboard: - self.dashboard.on_eval_end(params, cv_result) - - -class GenerationCallback(Callback): - """pymoo callback: updates dashboard + saves checkpoint after each generation.""" - - def __init__(self, dashboard: Optional[Dashboard] = None, - checkpoint_path: Optional[Path] = None, - problem: Optional["KalignCVProblem"] = None): - super().__init__() - self.dashboard = dashboard - self.checkpoint_path = checkpoint_path - self.problem = problem - - def notify(self, algorithm): - gen = algorithm.n_gen - - if self.dashboard: - self.dashboard.on_gen_start(gen) - - # Extract Pareto front from algorithm.opt - pareto = [] - if algorithm.opt is not None and len(algorithm.opt) > 0: - for ind in algorithm.opt: - params = decode_params(ind.X) - entry = { - "params": params, - "f1": -ind.F[0], - "tc": -ind.F[1], - } - if len(ind.F) > 2: - entry["wall_time"] = ind.F[2] - pareto.append(entry) - - if self.dashboard: - self.dashboard.on_gen_end(gen, pareto) - - # Save checkpoint after every generation - if self.checkpoint_path: - # Save population state (X, F) — not the algorithm object (has unpicklable locks) - pop = algorithm.pop - ckpt = { - "pop_X": pop.get("X").copy(), - "pop_F": pop.get("F").copy(), - "n_gen_completed": gen, - "history": self.problem.history if self.problem else [], - "pop_size": len(pop), - } - # Write to temp file then rename for atomicity - tmp = self.checkpoint_path.with_suffix(".tmp") - with open(tmp, "wb") as f: - pickle.dump(ckpt, f) - tmp.rename(self.checkpoint_path) - - -def load_checkpoint(path: Path): - """Load a generation checkpoint. Returns (pop_X, pop_F, n_gen_completed, history).""" - with open(path, "rb") as f: - ckpt = pickle.load(f) # noqa: S301 - return ckpt["pop_X"], ckpt["pop_F"], ckpt["n_gen_completed"], ckpt.get("history", []) - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser( - description="Optimize kalign hyperparameters with NSGA-II + stratified k-fold CV") - parser.add_argument("--pop-size", type=int, default=40, - help="Population size (default: 40)") - parser.add_argument("--n-gen", type=int, default=50, - help="Number of generations (default: 50)") - parser.add_argument("--n-folds", type=int, default=5, - help="Number of CV folds (default: 5)") - parser.add_argument("--n-threads", type=int, default=1, - help="OpenMP threads per kalign alignment (default: 1)") - parser.add_argument("--n-workers", type=int, default=1, - help="Parallel worker processes for evaluating individuals (default: 1)") - parser.add_argument("--seed", type=int, default=42, - help="Random seed (default: 42)") - parser.add_argument("--optimize-time", action="store_true", - help="Add wall time as 3rd objective") - parser.add_argument("--output-dir", type=str, default="benchmarks/results/optim", - help="Output directory for results") - parser.add_argument("--run-name", type=str, default=None, - help="Name for this run (creates subdirectory, e.g. 'run2_time')") - parser.add_argument("--no-dashboard", action="store_true", - help="Disable rich dashboard, use plain text output") - parser.add_argument("--resume", type=str, default=None, - help="Resume from a generation checkpoint file (.pkl)") - args = parser.parse_args() - - console = Console() - - # Setup - if not balibase_is_available(): - console.print("Downloading BAliBASE...") - balibase_download() - - cases = balibase_cases() - console.print(f"Loaded [bold]{len(cases)}[/] BAliBASE cases") - - # Show category distribution - cats: Dict[str, int] = {} - for c in cases: - cats[c.dataset] = cats.get(c.dataset, 0) + 1 - for cat, n in sorted(cats.items()): - console.print(f" {cat}: {n} cases") - - # Build stratified folds - k = args.n_folds - folds = stratified_kfold(cases, k, seed=args.seed) - console.print(f"\nStratified [bold]{k}[/]-fold CV:") - for i, (train, test) in enumerate(folds): - test_cats: Dict[str, int] = defaultdict(int) - for c in test: - test_cats[c.dataset] += 1 - cat_str = ", ".join(f"{cat.replace('balibase_', '')}:{n}" - for cat, n in sorted(test_cats.items())) - console.print(f" Fold {i}: {len(test)} test / {len(train)} train ({cat_str})") - - output_dir = Path(args.output_dir) - if args.run_name: - output_dir = output_dir / args.run_name - output_dir.mkdir(parents=True, exist_ok=True) - - # --- Baseline evaluation (CV) --- parallelized across folds - # Best F1 from first optimization run (pop_size=60, n_gen=100) - console.print(f"\n[bold]Baseline evaluation[/] (best from run 1, {k}-fold CV)") - baseline_params = { - "gap_open": 8.472, "gap_extend": 0.554, "terminal_gap_extend": 0.409, - "vsm_amax": 1.359, "seq_weights": 3.407, "consistency_weight": 1.167, - "consistency": 8, "realign": 2, "seq_type": "pfasum60", - } - n_baseline_workers = max(1, args.n_workers) - if n_baseline_workers > 1: - # Run CV folds + full-dataset eval in parallel - baseline_jobs = [] - for fi, (_, test) in enumerate(folds): - baseline_jobs.append((0, fi, encode_params(baseline_params), test, args.n_threads)) - # Add full-dataset as an extra "fold" - baseline_jobs.append((0, k, encode_params(baseline_params), cases, args.n_threads)) - console.print(f" Running {len(baseline_jobs)} baseline jobs in parallel ({n_baseline_workers} workers)...") - with ProcessPoolExecutor(max_workers=min(n_baseline_workers, len(baseline_jobs))) as pool: - futures = {pool.submit(_eval_one_fold, j): j[1] for j in baseline_jobs} - fold_results_bl = {} - for future in as_completed(futures): - _, fi, _, result = future.result() - fold_results_bl[fi] = result - fold_f1s = [fold_results_bl[fi]["f1"] for fi in range(k)] - fold_tcs = [fold_results_bl[fi]["tc"] for fi in range(k)] - total_time = sum(fold_results_bl[fi]["wall_time"] for fi in range(k)) - baseline_cv = { - "f1": float(np.mean(fold_f1s)), "tc": float(np.mean(fold_tcs)), - "f1_std": float(np.std(fold_f1s)), "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, "fold_tcs": fold_tcs, "wall_time": total_time, - } - baseline_full = fold_results_bl[k] # the extra "fold" with all cases - else: - baseline_cv = evaluate_cv(baseline_params, folds, args.n_threads) - baseline_full = evaluate_params(baseline_params, cases, args.n_threads) - - console.print(f" CV F1=[bold]{baseline_cv['f1']:.4f}[/]±{baseline_cv['f1_std']:.4f} " - f"CV TC=[bold]{baseline_cv['tc']:.4f}[/]±{baseline_cv['tc_std']:.4f} " - f"time={baseline_cv['wall_time']:.1f}s") - for i, (f1, tc) in enumerate(zip(baseline_cv['fold_f1s'], baseline_cv['fold_tcs'])): - console.print(f" Fold {i}: F1={f1:.4f} TC={tc:.4f}") - - console.print(f" Full-data F1=[bold]{baseline_full['f1']:.4f}[/] " - f"TC=[bold]{baseline_full['tc']:.4f}[/]") - for cat, v in sorted(baseline_full["per_category"].items()): - console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") - - # --- Optimization --- - n_evals = args.pop_size * args.n_gen - est_sec_per_eval = baseline_cv["wall_time"] - parallelism = max(1, args.n_workers) - est_hours = n_evals * est_sec_per_eval / parallelism / 3600 - - console.print(f"\n[bold]Starting NSGA-II[/]: pop_size={args.pop_size}, n_gen={args.n_gen}, " - f"{k}-fold CV, {args.n_workers} worker(s) × {args.n_threads} thread(s)") - console.print(f"Total evaluations: ~{n_evals} ({n_evals * len(cases)} alignments)") - console.print(f"Estimated time: ~{est_hours:.1f} hours " - f"(~{est_sec_per_eval:.0f}s per eval, " - f"{parallelism}× parallel)\n") - - # Set up dashboard or plain mode - use_dashboard = not args.no_dashboard - dashboard = None - - if use_dashboard: - dashboard = Dashboard( - n_gen=args.n_gen, - pop_size=args.pop_size, - baseline_f1=baseline_cv["f1"], - baseline_tc=baseline_cv["tc"], - optimize_time=args.optimize_time, - baseline_time=baseline_cv["wall_time"], - ) - - problem = KalignCVProblem( - folds=folds, - n_threads=args.n_threads, - n_workers=args.n_workers, - optimize_time=args.optimize_time, - dashboard=dashboard, - ) - - checkpoint_path = output_dir / "gen_checkpoint.pkl" - callback = GenerationCallback( - dashboard=dashboard, - checkpoint_path=checkpoint_path, - problem=problem, - ) - - # Resume from checkpoint or start fresh - resumed_gen = 0 - if args.resume: - resume_path = Path(args.resume) - if not resume_path.exists(): - console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") - return - pop_X, _pop_F, resumed_gen, prev_history = load_checkpoint(resume_path) - problem.history = prev_history - console.print(f"[bold green]Resumed[/] from generation {resumed_gen} " - f"({len(prev_history)} prior evaluations)") - remaining = args.n_gen - resumed_gen - if remaining <= 0: - console.print(f"[bold yellow]Already completed {resumed_gen} generations " - f"(requested {args.n_gen}). Increase --n-gen to continue.[/]") - return - termination = get_termination("n_gen", remaining) - # Reconstruct algorithm with saved population as initial sampling - algorithm = NSGA2( - pop_size=len(pop_X), - sampling=pop_X, - crossover=SBX(prob=0.9, eta=15), - mutation=PM(eta=20), - eliminate_duplicates=True, - ) - else: - algorithm = NSGA2( - pop_size=args.pop_size, - sampling=LHS(), - crossover=SBX(prob=0.9, eta=15), - mutation=PM(eta=20), - eliminate_duplicates=True, - ) - termination = get_termination("n_gen", args.n_gen) - - if dashboard: - dashboard.start() - - try: - res = minimize( - problem, - algorithm, - termination, - seed=args.seed, - verbose=not use_dashboard, # disable pymoo's own output when dashboard is active - callback=callback, - ) - except KeyboardInterrupt: - if dashboard: - dashboard.stop() - console.print("\n[bold yellow]Interrupted![/] Checkpoint was saved after last " - f"completed generation to:\n {checkpoint_path}") - console.print("Resume with: [bold]--resume " + str(checkpoint_path) + "[/]") - os._exit(1) # noqa: SLF001 — skip atexit handlers that hang on worker join - finally: - if dashboard: - dashboard.stop() - - # --- Results --- - console.print(f"\n[bold]Optimization complete.[/] " - f"{len(res.F)} Pareto-optimal solutions found.\n") - - pareto_configs = [] - for i, (x, f) in enumerate(zip(res.X, res.F)): - params = decode_params(x) - f1 = -f[0] - tc = -f[1] - wt = f[2] if args.optimize_time else None - pareto_configs.append({"params": params, "f1_cv": f1, "tc_cv": tc, "wall_time": wt}) - - # Print Pareto front as a rich table - table = Table(title="Pareto Front (sorted by CV F1)") - table.add_column("#", style="dim", width=3) - table.add_column("CV F1", justify="right") - table.add_column("CV TC", justify="right") - table.add_column("Parameters") - - sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) - for i, cfg in enumerate(sorted_pareto): - f1_style = "bold green" if cfg["f1_cv"] > baseline_cv["f1"] else "" - tc_style = "bold green" if cfg["tc_cv"] > baseline_cv["tc"] else "" - table.add_row( - str(i), - Text(f"{cfg['f1_cv']:.4f}", style=f1_style), - Text(f"{cfg['tc_cv']:.4f}", style=tc_style), - format_params_short(cfg["params"]), - ) - console.print(table) - - # --- Re-evaluate best on FULL dataset --- - best_f1_idx = np.argmin(res.F[:, 0]) - best = pareto_configs[best_f1_idx] - - console.print(f"\n{'='*60}") - console.print(f"[bold]Best CV F1 solution:[/] CV F1={best['f1_cv']:.4f} CV TC={best['tc_cv']:.4f}") - console.print(f" {format_params_long(best['params'])}") - - console.print(f"\n[bold]Full-dataset evaluation[/] (checking for overfit)") - best_full = evaluate_params(best["params"], cases, args.n_threads) - console.print(f" Full F1=[bold]{best_full['f1']:.4f}[/] Full TC=[bold]{best_full['tc']:.4f}[/] " - f"Recall={best_full['recall']:.4f} Precision={best_full['precision']:.4f}") - for cat, v in sorted(best_full["per_category"].items()): - console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") - - gap_f1 = best_full["f1"] - best["f1_cv"] - gap_tc = best_full["tc"] - best["tc_cv"] - console.print(f"\n Overfit check (full - CV): F1 {gap_f1:+.4f} TC {gap_tc:+.4f}") - if gap_f1 > 0.02: - console.print(" [bold yellow]Warning:[/] Full-data F1 notably higher than CV — possible overfit") - else: - console.print(" [bold green]OK:[/] Full-data and CV scores are consistent") - - # Comparison vs baseline - console.print(f"\n[bold]Improvement vs baseline[/] (full dataset)") - comp_table = Table() - comp_table.add_column("Category") - comp_table.add_column("F1 before", justify="right") - comp_table.add_column("F1 after", justify="right") - comp_table.add_column("ΔF1", justify="right") - comp_table.add_column("TC before", justify="right") - comp_table.add_column("TC after", justify="right") - comp_table.add_column("ΔTC", justify="right") - - for cat in sorted(set(list(baseline_full["per_category"].keys()) + - list(best_full["per_category"].keys()))): - b = baseline_full["per_category"].get(cat, {"f1": 0.0, "tc": 0.0}) - o = best_full["per_category"].get(cat, {"f1": 0.0, "tc": 0.0}) - df1 = o["f1"] - b["f1"] - dtc = o["tc"] - b["tc"] - f1_style = "green" if df1 > 0.005 else "red" if df1 < -0.005 else "" - tc_style = "green" if dtc > 0.005 else "red" if dtc < -0.005 else "" - comp_table.add_row( - cat.replace("balibase_", ""), - f"{b['f1']:.4f}", f"{o['f1']:.4f}", - Text(f"{df1:+.4f}", style=f1_style), - f"{b['tc']:.4f}", f"{o['tc']:.4f}", - Text(f"{dtc:+.4f}", style=tc_style), - ) - - df1 = best_full["f1"] - baseline_full["f1"] - dtc = best_full["tc"] - baseline_full["tc"] - f1_style = "bold green" if df1 > 0.005 else "bold red" if df1 < -0.005 else "bold" - tc_style = "bold green" if dtc > 0.005 else "bold red" if dtc < -0.005 else "bold" - comp_table.add_row( - "[bold]OVERALL[/]", - f"{baseline_full['f1']:.4f}", f"{best_full['f1']:.4f}", - Text(f"{df1:+.4f}", style=f1_style), - f"{baseline_full['tc']:.4f}", f"{best_full['tc']:.4f}", - Text(f"{dtc:+.4f}", style=tc_style), - ) - console.print(comp_table) - - # --- Save --- - checkpoint = { - "pareto_configs": pareto_configs, - "history": problem.history, - "baseline_cv": baseline_cv, - "baseline_full": baseline_full, - "best_full": best_full, - "folds_info": [(len(tr), len(te)) for tr, te in folds], - "args": vars(args), - } - ckpt_path = output_dir / "optim_checkpoint.pkl" - with open(ckpt_path, "wb") as f: - pickle.dump(checkpoint, f) - console.print(f"\nCheckpoint saved to {ckpt_path}") - - summary_path = output_dir / "pareto_front.txt" - with open(summary_path, "w") as f: - f.write("# Pareto-optimal kalign configs (NSGA-II + {}-fold stratified CV)\n".format(k)) - f.write(f"# pop_size={args.pop_size} n_gen={args.n_gen} " - f"n_cases={len(cases)} seed={args.seed}\n") - f.write(f"# Baseline CV: F1={baseline_cv['f1']:.4f} TC={baseline_cv['tc']:.4f}\n\n") - for i, cfg in enumerate(sorted_pareto): - p = cfg["params"] - f.write(f"[{i}] CV_F1={cfg['f1_cv']:.4f} CV_TC={cfg['tc_cv']:.4f}\n") - f.write(f" gap_open={p['gap_open']:.3f}\n") - f.write(f" gap_extend={p['gap_extend']:.3f}\n") - f.write(f" terminal_gap_extend={p['terminal_gap_extend']:.3f}\n") - f.write(f" vsm_amax={p['vsm_amax']:.3f}\n") - f.write(f" seq_weights={p['seq_weights']:.3f}\n") - f.write(f" consistency={p['consistency']}\n") - f.write(f" consistency_weight={p['consistency_weight']:.3f}\n") - f.write(f" realign={p['realign']}\n") - f.write(f" matrix={_matrix_name(p)}\n\n") - console.print(f"Pareto front saved to {summary_path}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/optimize_unified.py b/benchmarks/optimize_unified.py deleted file mode 100644 index cb660de..0000000 --- a/benchmarks/optimize_unified.py +++ /dev/null @@ -1,1746 +0,0 @@ -#!/usr/bin/env python3 -"""Unified multi-objective hyperparameter optimization for kalign using pymoo. - -Searches across kalign's entire operating range: from fast single-run alignment -through consistency-enhanced single-run to multi-run ensemble with POAR consensus. -Always uses three objectives: recall, precision, and wall time. The resulting 3D -Pareto surface reveals the full speed/accuracy trade-off landscape in one run. -Recall and precision are naturally in tension, yielding a richer Pareto front -than F1+TC. - -Objectives (always 3): - 1. Maximize recall (category-averaged SP score, held-out CV) - 2. Maximize precision (category-averaged, held-out CV) - 3. Minimize wall time (total CV evaluation time in seconds) - -Per-run decision variables (x max_runs slots): - - gpo: gap open penalty [2.0, 15.0] - - gpe: gap extend penalty [0.5, 5.0] - - tgpe: terminal gap extend [0.1, 3.0] - - noise: tree perturbation sigma [0.0, 0.5] - - matrix: substitution matrix {PFASUM43, PFASUM60, CorBLOSUM66} - - vsm_amax: variable scoring matrix [0.0, 5.0] - - refine: post-alignment refinement {NONE, ALL, CONFIDENT, INLINE} - -Shared decision variables: - - n_runs: {1, 3, 5} Core mode variable - - seq_weights: [0.0, 5.0] Profile rebalancing - - consistency: {0..6} Anchor consistency rounds - - consistency_weight: [0.5, 5.0] Consistency bonus weight - - realign: {0, 1, 2} Tree-rebuild iterations - - min_support: {0..max_runs} POAR consensus threshold - -Usage: - # Quick smoke test (protein/BAliBASE, default) - uv run python -m benchmarks.optimize_unified --pop-size 20 --n-gen 5 - - # Production run on BAliBASE (protein) - uv run python -m benchmarks.optimize_unified \\ - --pop-size 200 --n-gen 100 --n-workers 56 --n-threads 1 - - # Production run on BRAliBASE (RNA) - uv run python -m benchmarks.optimize_unified --dataset bralibase \\ - --pop-size 200 --n-gen 100 --n-workers 56 --n-threads 1 - - # Resume - uv run python -m benchmarks.optimize_unified \\ - --resume benchmarks/results/unified_optim/balibase/gen_checkpoint.pkl \\ - --n-gen 150 --n-workers 56 -""" - -import os -# Must set OMP_NUM_THREADS before importing kalign (or any C extension that -# uses OpenMP). ProcessPoolExecutor uses fork(), and forked children inherit -# the parent's OpenMP thread-pool state — but the pool threads are dead in the -# child. If OpenMP later tries to use them, the child segfaults. Setting -# this to "1" prevents the parent from ever creating extra threads, making -# fork safe. The actual per-alignment thread count is controlled by the -# n_threads parameter passed to kalign at call time. -if "OMP_NUM_THREADS" not in os.environ: - os.environ["OMP_NUM_THREADS"] = "1" - -import argparse -import pickle -import signal -import sys -import tempfile -import time -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -import numpy as np - -try: - from pymoo.algorithms.moo.nsga3 import NSGA3 # type: ignore[import-untyped] - from pymoo.core.callback import Callback # type: ignore[import-untyped] - from pymoo.core.mixed import ( # type: ignore[import-untyped] - MixedVariableDuplicateElimination, - MixedVariableMating, - MixedVariableSampling, - ) - from pymoo.core.population import Population # type: ignore[import-untyped] - from pymoo.core.problem import Problem # type: ignore[import-untyped] - from pymoo.core.variable import Choice, Integer, Real # type: ignore[import-untyped] - from pymoo.optimize import minimize # type: ignore[import-untyped] - from pymoo.termination import get_termination # type: ignore[import-untyped] - from pymoo.util.ref_dirs import get_reference_directions # type: ignore[import-untyped] -except ImportError: - print("pymoo not installed. Run: uv pip install pymoo") - sys.exit(1) - -from rich.console import Console # type: ignore[import-untyped] -from rich.layout import Layout # type: ignore[import-untyped] -from rich.live import Live # type: ignore[import-untyped] -from rich.panel import Panel # type: ignore[import-untyped] -from rich.progress import ( # type: ignore[import-untyped] - BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, -) -from rich.table import Table # type: ignore[import-untyped] -from rich.text import Text # type: ignore[import-untyped] - -from kalign._core import ( # type: ignore[import-untyped] - MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66, - MATRIX_DNA, MATRIX_RNA, - MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM, - REFINE_NONE, REFINE_ALL, - REFINE_CONFIDENT, REFINE_INLINE, ensemble_custom_file_to_file, -) - -from .datasets import (BenchmarkCase, - balibase_cases, balibase_download, balibase_is_available, - bralibase_cases, bralibase_download, bralibase_is_available, - mdsa_cases, mdsa_download, mdsa_is_available) -from .scoring import score_alignment_detailed - -# --------------------------------------------------------------------------- -# Parameter space definition -# --------------------------------------------------------------------------- - -# Maps for categorical variables (Choice options) -# These define the actual values; pymoo Choice handles selection directly. - -N_RUNS_MAP = [1, 3, 5] # index -> actual n_runs -CONSISTENCY_MAP = [0, 1, 2, 3, 5, 8, 10] -REFINE_MAP = [REFINE_NONE, REFINE_ALL, REFINE_CONFIDENT, REFINE_INLINE] -REFINE_NAMES = {REFINE_NONE: "N", REFINE_ALL: "A", REFINE_CONFIDENT: "C", REFINE_INLINE: "I"} -REFINE_LONG = {REFINE_NONE: "NONE", REFINE_ALL: "ALL", REFINE_CONFIDENT: "CONFIDENT", - REFINE_INLINE: "INLINE"} - -# --- Dataset-specific parameter space profiles --- - -PARAM_PROFILES = { - "protein": { - "per_run_cont_lower": np.array([2.0, 0.5, 0.1, 0.0]), - "per_run_cont_upper": np.array([15.0, 5.0, 3.0, 0.5]), - "shared_cont_lower": np.array([0.0, 0.0, 0.5]), - "shared_cont_upper": np.array([5.0, 5.0, 5.0]), - "matrix_map_int": [MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66], - "matrix_map_str": ["pfasum43", "pfasum60", "corblosum66"], - "matrix_names": {MATRIX_PFASUM43: "P43", MATRIX_PFASUM60: "P60", MATRIX_CORBLOSUM66: "CB66"}, - "n_matrices": 3, - "seq_type_int": MATRIX_PFASUM43, # default protein matrix for ensemble seq_type - "seq_type_str": "protein", # Python API string - "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range - }, - "rna": { - # RNA gap penalties tend to be different — wider search range for gpo - "per_run_cont_lower": np.array([1.0, 0.2, 0.05, 0.0]), - "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), - # vsm_amax: 0 by default for RNA, but let optimizer explore [0, 3] - # seq_weights: 0 by default for RNA, search [0, 3] - "shared_cont_lower": np.array([0.0, 0.0, 0.5]), - "shared_cont_upper": np.array([3.0, 3.0, 5.0]), - # Kimura 1/20/200 PAM matrices (shared with DNA) - "matrix_map_int": [MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM], - "matrix_map_str": ["nuc_1pam", "nuc_20pam", "nuc_200pam"], - "matrix_names": {MATRIX_NUC_1PAM: "K1", MATRIX_NUC_20PAM: "K20", MATRIX_NUC_200PAM: "K200"}, - "n_matrices": 3, - "seq_type_int": MATRIX_NUC_200PAM, - "seq_type_str": "rna", - "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range - }, - "dna": { - "per_run_cont_lower": np.array([1.0, 0.2, 0.05, 0.0]), - "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), - "shared_cont_lower": np.array([0.0, 0.0, 0.5]), - "shared_cont_upper": np.array([3.0, 3.0, 5.0]), - # Kimura 1/20/200 PAM matrices (shared with RNA) - "matrix_map_int": [MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM], - "matrix_map_str": ["nuc_1pam", "nuc_20pam", "nuc_200pam"], - "matrix_names": {MATRIX_NUC_1PAM: "K1", MATRIX_NUC_20PAM: "K20", MATRIX_NUC_200PAM: "K200"}, - "n_matrices": 3, - "seq_type_int": MATRIX_NUC_200PAM, - "seq_type_str": "dna", - "max_consistency_idx": len(CONSISTENCY_MAP) - 1, # full range - }, - "nucleotide": { - "per_run_cont_lower": np.array([1.0, 0.2, 0.05, 0.0]), - "per_run_cont_upper": np.array([20.0, 5.0, 3.0, 0.5]), - "shared_cont_lower": np.array([0.0, 0.0, 0.5]), - "shared_cont_upper": np.array([3.0, 3.0, 5.0]), - "matrix_map_int": [MATRIX_NUC_1PAM, MATRIX_NUC_20PAM, MATRIX_NUC_200PAM], - "matrix_map_str": ["nuc_1pam", "nuc_20pam", "nuc_200pam"], - "matrix_names": {MATRIX_NUC_1PAM: "K1", MATRIX_NUC_20PAM: "K20", MATRIX_NUC_200PAM: "K200"}, - "n_matrices": 3, - "seq_type_int": MATRIX_NUC_200PAM, - "seq_type_str": "nucleotide", - "max_consistency_idx": len(CONSISTENCY_MAP) - 1, - }, -} - -# Active profile (set by main() based on --dataset) -_active_profile = PARAM_PROFILES["protein"] - -def set_active_profile(profile_name: str): - global _active_profile - _active_profile = PARAM_PROFILES[profile_name] - -# Convenience accessors for the active profile -def _matrix_map_int(): return _active_profile["matrix_map_int"] -def _matrix_map_str(): return _active_profile["matrix_map_str"] -def _matrix_names(): return _active_profile["matrix_names"] - -# Legacy aliases (used in view_pareto.py etc.) -MATRIX_MAP_INT = PARAM_PROFILES["protein"]["matrix_map_int"] -MATRIX_MAP_STR = PARAM_PROFILES["protein"]["matrix_map_str"] -# Combined matrix names across all profiles (used by dashboard) -MATRIX_NAMES = {} -for _prof in PARAM_PROFILES.values(): - MATRIX_NAMES.update(_prof["matrix_names"]) - -# Old constant names used in view_pareto.py and checkpoint data — keep for loading old checkpoints -_OLD_MATRIX_COMPAT = {3: MATRIX_PFASUM43, 5: MATRIX_PFASUM43, 6: MATRIX_PFASUM60} - - -def get_vars(max_runs: int) -> dict: - """Return pymoo mixed-variable space definition. - - Per-run variables (one slot per max_runs): - gpo, gpe, tgpe, noise, matrix — gap/tree params - vsm_amax — variable scoring matrix amplitude - refine — post-alignment refinement mode - - Shared variables: - seq_weights, consistency_weight — scalars applied to all runs - n_runs, consistency, realign, min_support — integer/categorical - """ - profile = _active_profile - lo = profile["per_run_cont_lower"] - hi = profile["per_run_cont_upper"] - slo = profile["shared_cont_lower"] - shi = profile["shared_cont_upper"] - - variables = {} - for k in range(max_runs): - variables[f"gpo_{k}"] = Real(bounds=(float(lo[0]), float(hi[0]))) - variables[f"gpe_{k}"] = Real(bounds=(float(lo[1]), float(hi[1]))) - variables[f"tgpe_{k}"] = Real(bounds=(float(lo[2]), float(hi[2]))) - variables[f"noise_{k}"] = Real(bounds=(float(lo[3]), float(hi[3]))) - variables[f"matrix_{k}"] = Choice(options=list(range(profile["n_matrices"]))) - # Per-run VSM amplitude and refinement mode - variables[f"vsm_amax_{k}"] = Real(bounds=(float(slo[0]), float(shi[0]))) - variables[f"refine_{k}"] = Choice(options=REFINE_MAP) - - variables["seq_weights"] = Real(bounds=(float(slo[1]), float(shi[1]))) - variables["consistency_weight"] = Real(bounds=(float(slo[2]), float(shi[2]))) - - consistency_options = CONSISTENCY_MAP[:profile.get("max_consistency_idx", len(CONSISTENCY_MAP) - 1) + 1] - variables["n_runs"] = Choice(options=N_RUNS_MAP) - variables["consistency"] = Choice(options=consistency_options) - variables["realign"] = Integer(bounds=(0, 2)) - variables["min_support"] = Integer(bounds=(0, max_runs)) - variables["consistency_merge"] = Choice(options=[0, 1]) - variables["consistency_merge_weight"] = Real(bounds=(0.5, 10.0)) - - return variables - - -def decode_unified_params(x, max_runs: int): - """Decode a mixed-variable dict into a unified parameter dict. - - x is a dict with keys like 'gpo_0', 'n_runs', 'consistency', etc. - Values are native types (float for Real, int for Integer/Choice). - - vsm_amax and refine are per-run (vsm_amax_{k}, refine_{k}). - If the per-run keys are missing (old checkpoint), falls back to - shared 'vsm_amax' / 'refine' keys. - """ - n_runs = int(x["n_runs"]) - consistency = int(x["consistency"]) - realign = int(x["realign"]) - min_support_raw = int(x["min_support"]) - consistency_merge = int(x.get("consistency_merge", 0)) - consistency_merge_weight = float(x.get("consistency_merge_weight", 2.0)) - - seq_weights = float(x["seq_weights"]) - consistency_weight = float(x["consistency_weight"]) - - run_gpo, run_gpe, run_tgpe, run_noise = [], [], [], [] - run_types, run_matrices = [], [] - run_vsm_amax, run_refine = [], [] - - # Detect old checkpoint format (shared vsm_amax / refine) - has_per_run_vsm = f"vsm_amax_0" in x - has_per_run_refine = f"refine_0" in x - shared_vsm = float(x.get("vsm_amax", 0.0)) if not has_per_run_vsm else 0.0 - shared_refine = int(x.get("refine", REFINE_NONE)) if not has_per_run_refine else REFINE_NONE - - for k in range(n_runs): - run_gpo.append(float(x[f"gpo_{k}"])) - run_gpe.append(float(x[f"gpe_{k}"])) - run_tgpe.append(float(x[f"tgpe_{k}"])) - run_noise.append(float(x[f"noise_{k}"])) - matrix_idx = int(x[f"matrix_{k}"]) - run_types.append(_matrix_map_int()[matrix_idx]) - run_matrices.append(_matrix_map_str()[matrix_idx]) - run_vsm_amax.append(float(x[f"vsm_amax_{k}"]) if has_per_run_vsm else shared_vsm) - run_refine.append(int(x[f"refine_{k}"]) if has_per_run_refine else shared_refine) - - # --- Masking rules --- - - # Single-run: no tree noise (deterministic tree), no min_support - if n_runs == 1: - run_noise = [0.0] - min_support = 0 - else: - min_support = min(min_support_raw, n_runs) - - # When realign > 0, noise is ineffective (alignment-derived tree) - if realign > 0: - run_noise = [0.0] * n_runs - - # When consistency == 0, consistency_weight is irrelevant - if consistency == 0: - consistency_weight = 1.0 - - # Single-run: no consistency merge (needs ensemble) - if n_runs == 1: - consistency_merge = 0 - - # When consistency_merge == 0, weight is irrelevant - if consistency_merge == 0: - consistency_merge_weight = 2.0 - - return { - "n_runs": n_runs, - "run_gpo": run_gpo, - "run_gpe": run_gpe, - "run_tgpe": run_tgpe, - "run_noise": run_noise, - "run_types": run_types, - "run_matrices": run_matrices, - "run_vsm_amax": run_vsm_amax, - "run_refine": run_refine, - "seq_weights": seq_weights, - "consistency_weight": consistency_weight, - "consistency": consistency, - "realign": realign, - "min_support": min_support, - "consistency_merge": consistency_merge, - "consistency_merge_weight": consistency_merge_weight, - } - - -def encode_unified_params(params, max_runs: int) -> dict: - """Encode a unified params dict into a mixed-variable dict.""" - x: dict = {} - for k in range(max_runs): - if k < params["n_runs"]: - x[f"gpo_{k}"] = params["run_gpo"][k] - x[f"gpe_{k}"] = params["run_gpe"][k] - x[f"tgpe_{k}"] = params["run_tgpe"][k] - x[f"noise_{k}"] = params["run_noise"][k] - x[f"matrix_{k}"] = _matrix_map_int().index(params["run_types"][k]) - x[f"vsm_amax_{k}"] = params["run_vsm_amax"][k] - x[f"refine_{k}"] = params["run_refine"][k] - else: - x[f"gpo_{k}"] = params["run_gpo"][0] - x[f"gpe_{k}"] = params["run_gpe"][0] - x[f"tgpe_{k}"] = params["run_tgpe"][0] - x[f"noise_{k}"] = params["run_noise"][0] - x[f"matrix_{k}"] = _matrix_map_int().index(params["run_types"][0]) - x[f"vsm_amax_{k}"] = params["run_vsm_amax"][0] - x[f"refine_{k}"] = params["run_refine"][0] - - x["seq_weights"] = params["seq_weights"] - x["consistency_weight"] = params["consistency_weight"] - x["n_runs"] = params["n_runs"] - x["consistency"] = params["consistency"] - x["realign"] = params["realign"] - x["min_support"] = params["min_support"] - x["consistency_merge"] = params.get("consistency_merge", 0) - x["consistency_merge_weight"] = params.get("consistency_merge_weight", 2.0) - return x - - -def mode_label(params): - """Short mode label: 'single', 'ens3', 'ens5'.""" - n = params["n_runs"] - return "single" if n == 1 else f"ens{n}" - - -def format_unified_short(params): - """Compact one-line summary.""" - n_runs = params["n_runs"] - - if n_runs == 1: - mat = _matrix_names().get(params["run_types"][0], "?") - ref = REFINE_NAMES.get(params["run_refine"][0], "?") - return (f"{mode_label(params)} {mat} gpo={params['run_gpo'][0]:.1f} " - f"vsm={params['run_vsm_amax'][0]:.1f} sw={params['seq_weights']:.1f} " - f"c={params['consistency']} re={params['realign']} ref={ref}") - else: - # Show per-run refine modes compactly - refs = "/".join(REFINE_NAMES.get(r, "?") for r in params["run_refine"]) - vsms = "/".join(f"{v:.1f}" for v in params["run_vsm_amax"]) - cm = "CM" if params.get("consistency_merge", 0) else "POAR" - return (f"{mode_label(params)} vsm={vsms} " - f"sw={params['seq_weights']:.1f} c={params['consistency']} " - f"re={params['realign']} ref={refs} ms={params['min_support']} {cm}") - - -def format_unified_long(params): - """Verbose multi-line summary.""" - lines = [f"mode={mode_label(params)} n_runs={params['n_runs']}"] - for k in range(params["n_runs"]): - mat = _matrix_names().get(params["run_types"][k], "?") - ref = REFINE_LONG.get(params["run_refine"][k], "?") - lines.append(f" run_{k}: gpo={params['run_gpo'][k]:.3f} " - f"gpe={params['run_gpe'][k]:.3f} " - f"tgpe={params['run_tgpe'][k]:.3f} " - f"noise={params['run_noise'][k]:.3f} {mat} " - f"vsm={params['run_vsm_amax'][k]:.3f} ref={ref}") - lines.append(f" seq_weights={params['seq_weights']:.3f}") - lines.append(f" consistency={params['consistency']} " - f"consistency_weight={params['consistency_weight']:.3f}") - lines.append(f" realign={params['realign']} " - f"min_support={params['min_support']}") - if params.get("consistency_merge", 0): - lines.append(f" consistency_merge=1 " - f"consistency_merge_weight={params['consistency_merge_weight']:.3f}") - return "\n".join(lines) - - -# --------------------------------------------------------------------------- -# Stratified k-fold CV (shared with optimize_params) -# --------------------------------------------------------------------------- - -def stratified_kfold(cases: List[BenchmarkCase], k: int, seed: int = 42 - ) -> List[Tuple[List[BenchmarkCase], List[BenchmarkCase]]]: - """Split cases into k stratified folds by dataset (RV category).""" - rng = np.random.RandomState(seed) - - by_cat: Dict[str, List[BenchmarkCase]] = defaultdict(list) - for c in cases: - by_cat[c.dataset].append(c) - - fold_assignments: Dict[str, int] = {} - for cat_cases in by_cat.values(): - indices = list(range(len(cat_cases))) - rng.shuffle(indices) - for rank, idx in enumerate(indices): - fold_assignments[cat_cases[idx].family] = rank % k - - folds = [] - for fold_idx in range(k): - test = [c for c in cases if fold_assignments[c.family] == fold_idx] - train = [c for c in cases if fold_assignments[c.family] != fold_idx] - folds.append((train, test)) - - return folds - - -# --------------------------------------------------------------------------- -# Evaluation -# --------------------------------------------------------------------------- - -def evaluate_unified(params, cases, n_threads=1, quiet=True): - """Run kalign with unified params on all cases, return mean metrics.""" - results_by_cat: Dict[str, list] = {} - total_time = 0.0 - - for case in cases: - with tempfile.TemporaryDirectory() as tmpdir: - output = Path(tmpdir) / f"{case.family}_aln.fasta" - - try: - start = time.perf_counter() - - # Always use ensemble_custom_file_to_file — it handles - # n_runs=1 just fine and is the only path that accepts - # all fine-grained optimizer parameters. - ensemble_custom_file_to_file( - str(case.unaligned), - str(output), - run_gpo=params["run_gpo"], - run_gpe=params["run_gpe"], - run_tgpe=params["run_tgpe"], - run_noise=params["run_noise"], - run_types=params["run_types"], - format="fasta", - seq_type=_active_profile["seq_type_int"], - seed=42, - min_support=params["min_support"], - realign=params["realign"], - seq_weights=params["seq_weights"], - n_threads=n_threads, - consistency_anchors=params["consistency"], - consistency_weight=params["consistency_weight"], - # Per-run overrides - run_vsm_amax=params["run_vsm_amax"], - run_refine=params["run_refine"], - # MSA consistency merge - consistency_merge=params.get("consistency_merge", 0), - consistency_merge_weight=params.get("consistency_merge_weight", 2.0), - ) - - wall_time = time.perf_counter() - start - total_time += wall_time - - detailed = score_alignment_detailed(case.reference, output) - - cat = case.dataset - if cat not in results_by_cat: - results_by_cat[cat] = [] - results_by_cat[cat].append(detailed) - - except Exception as e: - if not quiet: - print(f" WARN: {case.family}: {e}", file=sys.stderr) - - if not results_by_cat: - return {"f1": 0.0, "tc": 0.0, "recall": 0.0, "precision": 0.0, - "wall_time": total_time, "per_category": {}} - - per_cat = {} - for cat, scores in results_by_cat.items(): - per_cat[cat] = { - "f1": np.mean([s["f1"] for s in scores]), - "tc": np.mean([s["tc"] for s in scores]), - "recall": np.mean([s["recall"] for s in scores]), - "precision": np.mean([s["precision"] for s in scores]), - "n": len(scores), - } - - all_f1 = [v["f1"] for v in per_cat.values()] - all_tc = [v["tc"] for v in per_cat.values()] - all_recall = [v["recall"] for v in per_cat.values()] - all_precision = [v["precision"] for v in per_cat.values()] - - return { - "f1": float(np.mean(all_f1)), - "tc": float(np.mean(all_tc)), - "recall": float(np.mean(all_recall)), - "precision": float(np.mean(all_precision)), - "wall_time": total_time, - "per_category": per_cat, - } - - -def fbeta_score(precision, recall, beta=1.0): - """Compute F-beta score: weights recall beta times more than precision.""" - if precision + recall == 0: - return 0.0 - b2 = beta * beta - return (1 + b2) * precision * recall / (b2 * precision + recall) - - -def evaluate_cv(params, folds, n_threads=1, quiet=True): - """Evaluate unified params using stratified k-fold CV.""" - fold_f1s = [] - fold_tcs = [] - fold_recalls = [] - fold_precisions = [] - total_time = 0.0 - - for _, test in folds: - result = evaluate_unified(params, test, n_threads, quiet) - fold_f1s.append(result["f1"]) - fold_tcs.append(result["tc"]) - fold_recalls.append(result["recall"]) - fold_precisions.append(result["precision"]) - total_time += result["wall_time"] - - return { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "recall": float(np.mean(fold_recalls)), - "precision": float(np.mean(fold_precisions)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, - "fold_tcs": fold_tcs, - "wall_time": total_time, - } - - -# --------------------------------------------------------------------------- -# Rich live dashboard -# --------------------------------------------------------------------------- - -class Dashboard: - """Rich-based live terminal dashboard for unified optimization.""" - - def __init__(self, n_gen: int, pop_size: int, - baselines: Dict[str, dict], - max_runs: int): - self.n_gen = n_gen - self.pop_size = pop_size - self.baselines = baselines # {"fast": {...}, "accurate": {...}, "ensemble": {...}} - self.max_runs = max_runs - self.console = Console() - - # State - self.current_gen = 0 - self.eval_count = 0 - self.total_evals = n_gen * pop_size - self.gen_start_time = time.time() - self.run_start_time = time.time() - self.current_eval_idx = 0 - - # Best-ever tracking - self.best_f1 = 0.0 - self.best_f1_entry: Optional[dict] = None - self.best_tc = 0.0 - self.best_tc_entry: Optional[dict] = None - self.fastest = float("inf") - self.fastest_entry: Optional[dict] = None - - # Pareto front (updated per generation) - self.pareto_front: List[dict] = [] - - # Generation history - self.gen_history: List[dict] = [] - - # Recent evaluations (ring buffer) - self.recent_evals: List[dict] = [] - - # Progress bar - self.progress = Progress( - SpinnerColumn(), - TextColumn("[bold blue]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeElapsedColumn(), - ) - self.gen_task = self.progress.add_task("Generations", total=n_gen) - self.eval_task = self.progress.add_task("Evaluations", total=pop_size) - - self.live = Live(self._build_layout(), console=self.console, refresh_per_second=2) - - def start(self): - self.run_start_time = time.time() - self.live.start() - - def stop(self): - self.live.stop() - - def _build_baselines_panel(self): - lines = [] - for name, bl in self.baselines.items(): - lines.append(f" {name:10s} F1={bl['f1']:.4f} TC={bl['tc']:.4f} " - f"Time={bl['wall_time']:.0f}s") - return Panel("\n".join(lines) if lines else "(computing...)", title="Baselines") - - def _build_status_panel(self): - elapsed = time.time() - self.run_start_time - elapsed_h = elapsed / 3600 - if self.eval_count > 0: - rate = elapsed / self.eval_count - remaining = (self.total_evals - self.eval_count) * rate - eta_h = remaining / 3600 - else: - rate = 0 - eta_h = 0 - - gen_elapsed = time.time() - self.gen_start_time - - text = (f"Gen {self.current_gen}/{self.n_gen} " - f"Eval {self.eval_count}/{self.total_evals}\n" - f"Elapsed {elapsed_h:.1f}h ETA {eta_h:.1f}h\n" - f"Gen time {gen_elapsed:.0f}s Rate {rate:.1f}s/eval") - return Panel(text, title="Progress") - - def _format_best_entry(self, label, e, delta_str): - """Format a best-of entry with full parameter details.""" - p = e["params"] - header = f"{label} {mode_label(p)} {e['wall_time']:.0f}s {delta_str}" - # Per-run details - run_parts = [] - for k in range(p["n_runs"]): - mat = MATRIX_NAMES.get(p["run_types"][k], "?") - ref = REFINE_NAMES.get(p["run_refine"][k], "?") - noise_str = f" n={p['run_noise'][k]:.2f}" if p["run_noise"][k] > 0 else "" - run_parts.append(f" R{k}: gpo={p['run_gpo'][k]:.2f} " - f"gpe={p['run_gpe'][k]:.2f} " - f"tgpe={p['run_tgpe'][k]:.2f}{noise_str} {mat} " - f"vsm={p['run_vsm_amax'][k]:.2f} ref={ref}") - shared = (f" sw={p['seq_weights']:.2f} " - f"c={p['consistency']} cw={p['consistency_weight']:.2f} " - f"re={p['realign']} ms={p['min_support']}") - return "\n".join([header] + run_parts + [shared]) - - def _build_best_panel(self): - lines = [] - if self.best_f1_entry: - e = self.best_f1_entry - lines.append(self._format_best_entry( - f"Best Recall: {e.get('recall', 0):.4f} F1={e['f1']:.4f} " - f"P={e.get('precision', 0):.3f} TC={e.get('tc', 0):.4f}", e, "")) - if self.best_tc_entry: - e = self.best_tc_entry - lines.append(self._format_best_entry( - f"Best Prec: {e.get('precision', 0):.4f} F1={e['f1']:.4f} " - f"R={e.get('recall', 0):.3f} TC={e.get('tc', 0):.4f}", e, "")) - if self.fastest_entry: - e = self.fastest_entry - lines.append(self._format_best_entry( - f"Fastest: {e['wall_time']:.1f}s F1={e['f1']:.4f} " - f"R={e.get('recall', 0):.3f} P={e.get('precision', 0):.3f}", e, "")) - return Panel("\n".join(lines) if lines else "(no data yet)", title="Best by Objective") - - def _build_pareto_table(self): - table = Table(title="Pareto Front (top 12 by F1)", box=None, padding=(0, 1)) - table.add_column("#", style="dim", width=2) - table.add_column("Mode", width=6) - table.add_column("Recall", justify="right", width=6) - table.add_column("Prec", justify="right", width=6) - table.add_column("F1", justify="right", width=6) - table.add_column("Time", justify="right", width=5) - table.add_column("c", width=2) - table.add_column("re", width=2) - table.add_column("ref", width=3) - table.add_column("Params", no_wrap=True) - - sorted_front = sorted(self.pareto_front, key=lambda x: -x["f1"])[:12] - bl_recall = max(bl.get("recall", 0) for bl in self.baselines.values()) if self.baselines else 0 - bl_prec = max(bl.get("precision", 0) for bl in self.baselines.values()) if self.baselines else 0 - - for i, entry in enumerate(sorted_front): - p = entry.get("params", {}) - r_style = "bold green" if entry.get("recall", 0) > bl_recall else "" - p_style = "bold green" if entry.get("precision", 0) > bl_prec else "" - refs = "/".join(REFINE_NAMES.get(r, "?") for r in p.get("run_refine", [0])) - - # Build detailed params string - run_strs = [] - for k in range(p.get("n_runs", 1)): - mat = MATRIX_NAMES.get(p["run_types"][k], "?") - vsm = p["run_vsm_amax"][k] - run_strs.append(f"R{k}:{p['run_gpo'][k]:.1f}/{p['run_gpe'][k]:.2f}/" - f"{p['run_tgpe'][k]:.2f}/{mat}/v{vsm:.1f}") - runs = " ".join(run_strs) - cm = " CM" if p.get("consistency_merge", 0) else "" - shared = (f"sw={p.get('seq_weights', 0):.1f} " - f"ms={p.get('min_support', 0)}{cm}") - params_str = f"{runs} | {shared}" - - table.add_row( - str(i), - mode_label(p), - Text(f"{entry.get('recall', 0):.4f}", style=r_style), - Text(f"{entry.get('precision', 0):.4f}", style=p_style), - f"{entry['f1']:.4f}", - f"{entry.get('wall_time', 0):.0f}s", - str(p.get("consistency", 0)), - str(p.get("realign", 0)), - refs, - params_str, - ) - return table - - def _build_mode_panel(self): - # Count modes in Pareto front and recent - pareto_modes: Dict[str, int] = defaultdict(int) - for entry in self.pareto_front: - pareto_modes[mode_label(entry.get("params", {}))] += 1 - parts = [f"{m}={n}" for m, n in sorted(pareto_modes.items())] - return Panel(f"Pareto: {' '.join(parts)}" if parts else "(none)", - title="Mode Distribution") - - def _build_trend_panel(self): - lines = [] - for h in self.gen_history[-6:]: - lines.append(f"Gen {h['gen']:3d}: F1={h['best_f1']:.4f} " - f"R={h.get('best_recall', 0):.4f} " - f"P={h.get('best_prec', 0):.4f} " - f"n_pareto={h['n_pareto']}") - return Panel("\n".join(lines) if lines else "(no data)", title="Trend") - - def _build_recent_panel(self): - lines = [] - for e in self.recent_evals[-5:]: - p = e.get("params", {}) - ref0 = REFINE_NAMES.get(p["run_refine"][0], "?") if p.get("run_refine") else "?" - mat = MATRIX_NAMES.get(p["run_types"][0], "?") if p.get("run_types") else "?" - gpo = p["run_gpo"][0] if p.get("run_gpo") else 0 - vsm0 = p["run_vsm_amax"][0] if p.get("run_vsm_amax") else 0 - lines.append(f"F1={e['f1']:.4f} TC={e['tc']:.4f} " - f"t={e['wall_time']:.0f}s {mode_label(p)} " - f"gpo={gpo:.1f} {mat} " - f"vsm={vsm0:.1f} " - f"c={p.get('consistency', 0)} re={p.get('realign', 0)} ref={ref0}") - return Panel("\n".join(lines) if lines else "(none)", title="Recent") - - def _build_layout(self): - layout = Layout() - layout.split_column( - Layout(name="top", size=6), - Layout(name="baselines", size=5), - Layout(name="best", size=24), - Layout(name="middle", size=16), - Layout(name="bottom", size=8), - ) - layout["top"].split_row( - Layout(self._build_status_panel(), name="status"), - Layout(self.progress, name="progress"), - ) - layout["baselines"].update(self._build_baselines_panel()) - layout["best"].update(self._build_best_panel()) - layout["middle"].split_row( - Layout(self._build_pareto_table(), name="pareto", ratio=3), - Layout(self._build_trend_panel(), name="trend", ratio=2), - ) - layout["bottom"].split_row( - Layout(self._build_mode_panel(), name="modes"), - Layout(self._build_recent_panel(), name="recent", ratio=2), - ) - return layout - - def refresh(self): - self.live.update(self._build_layout()) - - def on_eval_start(self, params: dict, eval_num: int, eval_in_gen: int): - self.eval_count = eval_num - self.current_eval_idx = eval_in_gen - self.progress.update(self.eval_task, completed=eval_in_gen) - self.refresh() - - def on_eval_end(self, params: dict, cv_result: dict): - recall = cv_result.get("recall", 0.0) - precision = cv_result.get("precision", 0.0) - f1 = fbeta_score(precision, recall, 1.0) - tc = cv_result.get("tc", 0.0) - wt = cv_result.get("wall_time", 0.0) - - entry = {"params": params, "f1": f1, "tc": tc, "wall_time": wt, - "recall": recall, "precision": precision} - self.recent_evals.append(entry) - if len(self.recent_evals) > 5: - self.recent_evals.pop(0) - - if recall > self.best_f1: # reuse field for best recall - self.best_f1 = recall - self.best_f1_entry = entry - if precision > self.best_tc: # reuse field for best precision - self.best_tc = precision - self.best_tc_entry = entry - if wt < self.fastest and f1 > 0.5: - self.fastest = wt - self.fastest_entry = entry - - self.refresh() - - def on_gen_start(self, gen: int): - self.current_gen = gen - self.gen_start_time = time.time() - self.progress.update(self.gen_task, completed=gen) - self.progress.update(self.eval_task, completed=0) - self.refresh() - - def on_gen_end(self, gen: int, pareto_front: List[dict]): - self.pareto_front = pareto_front - - best_recall = max((s.get("recall", 0) for s in pareto_front), default=0.0) - best_prec = max((s.get("precision", 0) for s in pareto_front), default=0.0) - best_f1_in_gen = max((s["f1"] for s in pareto_front), default=0.0) - self.gen_history.append({ - "gen": gen, - "best_f1": best_f1_in_gen, - "best_recall": best_recall, - "best_prec": best_prec, - "n_pareto": len(pareto_front), - }) - - self.progress.update(self.gen_task, completed=gen) - self.refresh() - - -# --------------------------------------------------------------------------- -# Parallel evaluation helper (must be top-level for pickling) -# --------------------------------------------------------------------------- - -def _eval_one_unified_fold(args_tuple): - """Evaluate one (individual, fold) pair.""" - ind_idx, fold_idx, x, test_cases, n_threads, max_runs = args_tuple - import faulthandler, sys - faulthandler.enable(file=sys.stderr) - params = decode_unified_params(x, max_runs) - result = evaluate_unified(params, test_cases, n_threads, quiet=True) - return ind_idx, fold_idx, params, result - - -def _eval_baseline(args_tuple): - """Evaluate one baseline configuration on a set of cases.""" - name, fi, bl_params, test_cases, n_threads = args_tuple - import faulthandler, sys - faulthandler.enable(file=sys.stderr) - result = evaluate_unified(bl_params, test_cases, n_threads, quiet=True) - return name, fi, result - - -def _kill_pool(pool: ProcessPoolExecutor) -> None: - """Forcefully terminate all worker processes.""" - for pid in list(pool._processes): # noqa: SLF001 - try: - os.kill(pid, signal.SIGTERM) - except OSError: - pass - pool.shutdown(wait=False, cancel_futures=True) - - -# --------------------------------------------------------------------------- -# pymoo Problem + Callback -# --------------------------------------------------------------------------- - -class UnifiedCVProblem(Problem): - """3-objective optimization with stratified CV evaluation.""" - - def __init__(self, folds, max_runs: int, n_threads=1, n_workers=1, - dashboard: Optional[Dashboard] = None, f_beta: float = 1.0): - super().__init__( - vars=get_vars(max_runs), - n_obj=3, # always 3: -recall, -precision, time - ) - self.folds = folds - self.max_runs = max_runs - self.n_threads = n_threads - self.n_workers = n_workers - self.dashboard = dashboard - self.f_beta = f_beta - self.eval_count = 0 - self.history: List[dict] = [] - - def _evaluate(self, X, out, *_args, **_kwargs): - F = np.zeros((len(X), 3)) - - if self.n_workers > 1: - self._evaluate_parallel(X, F) - else: - self._evaluate_serial(X, F) - - out["F"] = F - - def _evaluate_serial(self, X, F): - for i, x in enumerate(X): - params = decode_unified_params(x, self.max_runs) - self.eval_count += 1 - - if self.dashboard: - self.dashboard.on_eval_start(params, self.eval_count, i) - - cv_result = evaluate_cv(params, self.folds, self.n_threads, quiet=True) - self._record(i, F, params, cv_result) - - def _evaluate_parallel(self, X, F): - """Fine-grained parallelism: submit (individual x fold) jobs.""" - n_folds = len(self.folds) - - jobs = [] - for i, x in enumerate(X): - for fold_idx, (_, test) in enumerate(self.folds): - jobs.append((i, fold_idx, x, test, self.n_threads, self.max_runs)) - - if self.dashboard: - self.dashboard.on_eval_start({}, self.eval_count + 1, 0) - - fold_results: Dict[int, Dict[int, dict]] = defaultdict(dict) - ind_params: Dict[int, dict] = {} - - pool = ProcessPoolExecutor(max_workers=self.n_workers) - try: - futures = {pool.submit(_eval_one_unified_fold, j): j[:2] for j in jobs} - completed_individuals = set() - - for future in as_completed(futures): - ind_idx, fold_idx, params, result = future.result() - fold_results[ind_idx][fold_idx] = result - ind_params[ind_idx] = params - - if len(fold_results[ind_idx]) == n_folds: - completed_individuals.add(ind_idx) - self.eval_count += 1 - - fold_f1s = [fold_results[ind_idx][fi]["f1"] for fi in range(n_folds)] - fold_tcs = [fold_results[ind_idx][fi]["tc"] for fi in range(n_folds)] - fold_recalls = [fold_results[ind_idx][fi]["recall"] for fi in range(n_folds)] - fold_precisions = [fold_results[ind_idx][fi]["precision"] for fi in range(n_folds)] - total_time = sum(fold_results[ind_idx][fi]["wall_time"] - for fi in range(n_folds)) - - cv_result = { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "recall": float(np.mean(fold_recalls)), - "precision": float(np.mean(fold_precisions)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "fold_f1s": fold_f1s, - "fold_tcs": fold_tcs, - "wall_time": total_time, - } - - self._record(ind_idx, F, params, cv_result) - - if self.dashboard: - self.dashboard.on_eval_start( - params, self.eval_count, len(completed_individuals)) - - except KeyboardInterrupt: - _kill_pool(pool) - raise - finally: - pool.shutdown(wait=False) - - def _record(self, i, F, params, cv_result): - fb = fbeta_score(cv_result["precision"], cv_result["recall"], self.f_beta) - cv_result["fbeta"] = fb - F[i, 0] = -cv_result["recall"] - F[i, 1] = -cv_result["precision"] - F[i, 2] = cv_result["wall_time"] - - self.history.append({ - "eval": self.eval_count, - "params": params, - "cv_result": cv_result, - }) - - if self.dashboard: - self.dashboard.on_eval_end(params, cv_result) - - -class GenerationCallback(Callback): - """pymoo callback: updates dashboard + saves checkpoint after each generation.""" - - def __init__(self, dashboard: Optional[Dashboard] = None, - checkpoint_path: Optional[Path] = None, - problem: Optional["UnifiedCVProblem"] = None, - max_runs: int = 5): - super().__init__() - self.dashboard = dashboard - self.checkpoint_path = checkpoint_path - self.problem = problem - self.max_runs = max_runs - - def notify(self, algorithm): - gen = algorithm.n_gen - - if self.dashboard: - self.dashboard.on_gen_start(gen) - - # Extract Pareto front - pareto = [] - if algorithm.opt is not None and len(algorithm.opt) > 0: - for ind in algorithm.opt: - params = decode_unified_params(ind.X, self.max_runs) - recall = -ind.F[0] - precision = -ind.F[1] - f1 = fbeta_score(precision, recall, 1.0) - entry = { - "params": params, - "recall": recall, - "precision": precision, - "f1": f1, - "tc": 0.0, # not an objective; filled from cv_result if available - "wall_time": ind.F[2], - } - pareto.append(entry) - - if self.dashboard: - self.dashboard.on_gen_end(gen, pareto) - - # Save checkpoint - if self.checkpoint_path: - pop = algorithm.pop - # Deep-copy dicts in X (mixed-variable: array of dicts) - raw_X = pop.get("X") - pop_X = np.array([dict(d) for d in raw_X], dtype=object) - ckpt = { - "format": "mixed_v2", - "pop_X": pop_X, - "pop_F": pop.get("F").copy(), - "pop_G": pop.get("G"), - "pop_H": pop.get("H"), - "n_gen_completed": gen, - "history": self.problem.history if self.problem else [], - "pop_size": len(pop), - "max_runs": self.max_runs, - "profile": _active_profile.get("seq_type_str", "protein"), - "f_beta": self.problem.f_beta if self.problem else 1.0, - } - tmp = self.checkpoint_path.with_suffix(".tmp") - with open(tmp, "wb") as f: - pickle.dump(ckpt, f) - tmp.rename(self.checkpoint_path) - - -def load_checkpoint(path: Path): - """Load a generation checkpoint. - - Backfills missing keys for new decision variables so old checkpoints - remain compatible with the current variable space. - """ - with open(path, "rb") as f: - ckpt = pickle.load(f) # noqa: S301 - - # Backfill new variables added after the checkpoint was saved. - # Each individual in pop_X is a dict of decision variables. - # pymoo's crossover directly indexes parent.X[var_name], so ALL - # variables in get_vars() must exist in every individual dict. - max_runs = ckpt.get("max_runs", 5) - pop_X = ckpt.get("pop_X") - if pop_X is not None: - for x in pop_X: - if not isinstance(x, dict): - continue - - # v1 → v2: shared vsm_amax/refine → per-run arrays - if "vsm_amax_0" not in x: - shared_vsm = float(x.get("vsm_amax", 0.0)) - shared_ref = int(x.get("refine", REFINE_NONE)) - for k in range(max_runs): - x[f"vsm_amax_{k}"] = shared_vsm - x[f"refine_{k}"] = shared_ref - - # MSA consistency merge (added v3.6) - if "consistency_merge" not in x: - x["consistency_merge"] = 0 - if "consistency_merge_weight" not in x: - x["consistency_merge_weight"] = 2.0 - - return ckpt - - -# --------------------------------------------------------------------------- -# Baseline definitions -# --------------------------------------------------------------------------- - -BASELINE_CONFIGS_PROTEIN = { - "fast": { - "n_runs": 1, - "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [MATRIX_PFASUM60], "run_matrices": ["pfasum60"], - "run_vsm_amax": [2.0], "run_refine": [REFINE_NONE], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 0, "min_support": 0, - }, - "accurate": { - "n_runs": 1, - "run_gpo": [8.472], "run_gpe": [0.554], "run_tgpe": [0.409], "run_noise": [0.0], - "run_types": [MATRIX_PFASUM60], "run_matrices": ["pfasum60"], - "run_vsm_amax": [1.359], "run_refine": [REFINE_NONE], - "seq_weights": 3.407, "consistency_weight": 1.167, - "consistency": 8, "realign": 2, "min_support": 0, - }, - "ensemble": { - "n_runs": 3, - "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], - "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], - "run_types": [MATRIX_PFASUM60, MATRIX_PFASUM60, MATRIX_PFASUM60], - "run_matrices": ["pfasum60", "pfasum60", "pfasum60"], - "run_vsm_amax": [2.0, 2.0, 2.0], - "run_refine": [REFINE_CONFIDENT, REFINE_CONFIDENT, REFINE_CONFIDENT], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 1, "min_support": 0, - }, -} - -BASELINE_CONFIGS_RNA = { - "fast": { - "n_runs": 1, - "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [MATRIX_RNA], "run_matrices": ["rna"], - "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 0, "min_support": 0, - }, - "accurate": { - "n_runs": 1, - "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [MATRIX_RNA], "run_matrices": ["rna"], - "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 8, "realign": 2, "min_support": 0, - }, - "ensemble": { - "n_runs": 3, - "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], - "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], - "run_types": [MATRIX_RNA, MATRIX_RNA, MATRIX_RNA], "run_matrices": ["rna", "rna", "rna"], - "run_vsm_amax": [0.0, 0.0, 0.0], - "run_refine": [REFINE_CONFIDENT, REFINE_CONFIDENT, REFINE_CONFIDENT], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 1, "min_support": 0, - }, -} - -BASELINE_CONFIGS_DNA = { - "fast": { - "n_runs": 1, - "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [MATRIX_DNA], "run_matrices": ["dna"], - "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 0, "realign": 0, "min_support": 0, - }, - "accurate": { - "n_runs": 1, - "run_gpo": [7.0], "run_gpe": [1.25], "run_tgpe": [1.0], "run_noise": [0.0], - "run_types": [MATRIX_DNA], "run_matrices": ["dna"], - "run_vsm_amax": [0.0], "run_refine": [REFINE_NONE], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 8, "realign": 2, "min_support": 0, - }, - "ensemble": { - "n_runs": 3, - "run_gpo": [7.0, 3.5, 10.5], "run_gpe": [1.25, 2.5, 0.625], - "run_tgpe": [1.0, 2.0, 0.5], "run_noise": [0.0, 0.15, 0.15], - "run_types": [MATRIX_DNA, MATRIX_DNA, MATRIX_DNA], "run_matrices": ["dna", "dna", "dna"], - "run_vsm_amax": [0.0, 0.0, 0.0], - "run_refine": [REFINE_CONFIDENT, REFINE_CONFIDENT, REFINE_CONFIDENT], - "seq_weights": 0.0, "consistency_weight": 2.0, - "consistency": 8, "realign": 1, "min_support": 0, - }, -} - -def get_baseline_configs(dataset: str) -> dict: - """Return baseline configs appropriate for the dataset.""" - if dataset == "bralibase": - return BASELINE_CONFIGS_RNA - if dataset in ("mdsa", "nucleotide"): - return BASELINE_CONFIGS_DNA - return BASELINE_CONFIGS_PROTEIN - -# Default for backward compat -BASELINE_CONFIGS = BASELINE_CONFIGS_PROTEIN - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser( - description="Unified kalign hyperparameter optimization with NSGA-II (3 objectives)") - parser.add_argument("--max-runs", type=int, default=5, - help="Max ensemble runs. n_runs choices: {1,3,5} (default: 5)") - parser.add_argument("--pop-size", type=int, default=200, - help="Population size (default: 200)") - parser.add_argument("--n-gen", type=int, default=100, - help="Number of generations (default: 100)") - parser.add_argument("--n-folds", type=int, default=5, - help="Number of CV folds (default: 5)") - parser.add_argument("--n-threads", type=int, default=1, - help="OpenMP threads per kalign alignment (default: 1)") - parser.add_argument("--n-workers", type=int, default=1, - help="Parallel worker processes (default: 1)") - parser.add_argument("--seed", type=int, default=42, - help="Random seed (default: 42)") - parser.add_argument("--output-dir", type=str, default="benchmarks/results/unified_optim", - help="Output directory for results") - parser.add_argument("--run-name", type=str, default=None, - help="Name for this run (creates subdirectory)") - parser.add_argument("--no-dashboard", action="store_true", - help="Disable rich dashboard, use plain text output") - parser.add_argument("--f-beta", type=float, default=1.0, - help="F-beta weight for recall vs precision. " - "1.0=standard F1, 1.5=recall-weighted, 2.0=F2 (default: 1.0)") - parser.add_argument("--dataset", type=str, default="balibase", - choices=["balibase", "bralibase", "mdsa", "nucleotide"], - help="Benchmark dataset (default: balibase)") - parser.add_argument("--resume", type=str, default=None, - help="Resume from a generation checkpoint file (.pkl)") - parser.add_argument("--seed-from", type=str, default=None, - help="Seed initial population from another checkpoint (e.g. a beta=1.0 run). " - "Parameters are kept, fitness is re-evaluated with current settings.") - args = parser.parse_args() - - console = Console() - - # --- Dataset setup --- - if args.dataset == "balibase": - set_active_profile("protein") - if not balibase_is_available(): - console.print("Downloading BAliBASE...") - balibase_download() - cases = balibase_cases() - elif args.dataset == "bralibase": - set_active_profile("rna") - if not bralibase_is_available(): - console.print("Downloading BRAliBASE...") - bralibase_download() - cases = bralibase_cases() - elif args.dataset == "mdsa": - set_active_profile("dna") - if not mdsa_is_available(): - console.print("Downloading MDSA...") - mdsa_download() - cases = mdsa_cases() - elif args.dataset == "nucleotide": - set_active_profile("nucleotide") - if not bralibase_is_available(): - console.print("Downloading BRAliBASE...") - bralibase_download() - if not mdsa_is_available(): - console.print("Downloading MDSA...") - mdsa_download() - cases = bralibase_cases() + mdsa_cases() - console.print(f" Combined: {len([c for c in cases if c.seq_type == 'rna'])} RNA + " - f"{len([c for c in cases if c.seq_type == 'dna'])} DNA cases") - else: - console.print(f"[bold red]Unknown dataset: {args.dataset}[/]") - return - - # --- Run configuration banner --- - variables = get_vars(args.max_runs) - var_names = sorted(variables.keys()) - has_cm = "consistency_merge" in variables - console.print() - console.print("[bold cyan]=" * 72) - console.print("[bold cyan] Kalign Unified Optimizer") - console.print("[bold cyan]=" * 72) - console.print(f" Dataset: [bold]{args.dataset}[/] ({_active_profile['seq_type_str']})") - console.print(f" Pop size: {args.pop_size} Generations: {args.n_gen}") - console.print(f" Workers: {args.n_workers} Threads/eval: {args.n_threads}") - console.print(f" Max runs: {args.max_runs}") - console.print(f" Objectives: recall, precision, time") - console.print(f" Variables: {len(var_names)}") - console.print(f" Resume: {args.resume or 'no'}") - console.print(f" Seed from: {args.seed_from or 'no'}") - cm_status = "[bold green]ENABLED[/]" if has_cm else "[bold red]DISABLED[/]" - console.print(f" Consistency merge: {cm_status}") - if has_cm: - console.print(f" - consistency_merge: Choice([0, 1])") - console.print(f" - consistency_merge_weight: Real([0.5, 10.0])") - console.print("[bold cyan]" + "-" * 72) - console.print() - - console.print(f"Loaded [bold]{len(cases)}[/] {args.dataset} cases " - f"(profile: {_active_profile['seq_type_str']})") - - cats: Dict[str, int] = {} - for c in cases: - cats[c.dataset] = cats.get(c.dataset, 0) + 1 - for cat, n in sorted(cats.items()): - console.print(f" {cat}: {n} cases") - - # Build stratified folds - k = args.n_folds - folds = stratified_kfold(cases, k, seed=args.seed) - console.print(f"\nStratified [bold]{k}[/]-fold CV:") - for i, (train, test) in enumerate(folds): - test_cats: Dict[str, int] = defaultdict(int) - for c in test: - test_cats[c.dataset] += 1 - cat_str = ", ".join(f"{cat.replace('balibase_', '')}:{n}" - for cat, n in sorted(test_cats.items())) - console.print(f" Fold {i}: {len(test)} test / {len(train)} train ({cat_str})") - - output_dir = Path(args.output_dir) - if args.run_name: - output_dir = output_dir / args.run_name - else: - output_dir = output_dir / args.dataset - output_dir.mkdir(parents=True, exist_ok=True) - - max_runs = args.max_runs - - # --- Baseline evaluations (parallelized) --- - bl_configs = get_baseline_configs(args.dataset) - console.print(f"\n[bold]Baseline evaluations[/] ({k}-fold CV)") - n_baseline_workers = max(1, args.n_workers) - baselines: Dict[str, dict] = {} - - # Build all baseline jobs: (name, fold_idx, cases) - baseline_jobs = [] - for name, bl_params in bl_configs.items(): - for fi, (_, test) in enumerate(folds): - baseline_jobs.append((name, fi, bl_params, test, args.n_threads)) - # Full-dataset as extra fold - baseline_jobs.append((name, k, bl_params, cases, args.n_threads)) - - if n_baseline_workers > 1: - console.print(f" Running {len(baseline_jobs)} baseline jobs in parallel " - f"({n_baseline_workers} workers)...") - bl_fold_results: Dict[str, Dict[int, dict]] = defaultdict(dict) - with ProcessPoolExecutor(max_workers=min(n_baseline_workers, len(baseline_jobs))) as pool: - futures = {pool.submit(_eval_baseline, j): j[:2] for j in baseline_jobs} - for future in as_completed(futures): - name, fi, result = future.result() - bl_fold_results[name][fi] = result - else: - bl_fold_results = defaultdict(dict) - for j in baseline_jobs: - name, fi, result = _eval_baseline(j) - bl_fold_results[name][fi] = result - - for name in bl_configs: - fold_f1s = [bl_fold_results[name][fi]["f1"] for fi in range(k)] - fold_tcs = [bl_fold_results[name][fi]["tc"] for fi in range(k)] - total_time = sum(bl_fold_results[name][fi]["wall_time"] for fi in range(k)) - baselines[name] = { - "f1": float(np.mean(fold_f1s)), - "tc": float(np.mean(fold_tcs)), - "f1_std": float(np.std(fold_f1s)), - "tc_std": float(np.std(fold_tcs)), - "wall_time": total_time, - } - bl_full = bl_fold_results[name][k] - console.print(f" {name:10s} CV F1={baselines[name]['f1']:.4f}+-{baselines[name]['f1_std']:.4f} " - f"CV TC={baselines[name]['tc']:.4f} " - f"Time={baselines[name]['wall_time']:.0f}s " - f"Full F1={bl_full['f1']:.4f}") - - # --- Optimization --- - n_evals = args.pop_size * args.n_gen - # Use accurate baseline time as estimate - est_sec_per_eval = baselines.get("accurate", {}).get("wall_time", 100) - parallelism = max(1, args.n_workers) - est_hours = n_evals * est_sec_per_eval / parallelism / 3600 - - console.print(f"\n[bold]Starting NSGA-III[/]: pop_size={args.pop_size}, n_gen={args.n_gen}, " - f"{k}-fold CV, {args.n_workers} worker(s) x {args.n_threads} thread(s)") - console.print(f"Total evaluations: ~{n_evals}") - console.print(f"Estimated time: ~{est_hours:.1f} hours " - f"(~{est_sec_per_eval:.0f}s per eval, {parallelism}x parallel)\n") - - # Set up dashboard or plain mode - use_dashboard = not args.no_dashboard - dashboard = None - - if use_dashboard: - dashboard = Dashboard( - n_gen=args.n_gen, - pop_size=args.pop_size, - baselines=baselines, - max_runs=max_runs, - ) - - if args.f_beta != 1.0: - console.print(f"[bold yellow]Using F-beta objective with β={args.f_beta}[/] " - f"(recall weighted {args.f_beta}x more than precision)") - - problem = UnifiedCVProblem( - folds=folds, - max_runs=max_runs, - n_threads=args.n_threads, - n_workers=args.n_workers, - dashboard=dashboard, - f_beta=args.f_beta, - ) - - checkpoint_path = output_dir / "gen_checkpoint.pkl" - callback = GenerationCallback( - dashboard=dashboard, - checkpoint_path=checkpoint_path, - problem=problem, - max_runs=max_runs, - ) - - # NSGA-III reference directions for 3 objectives - ref_dirs = get_reference_directions("das-dennis", 3, n_partitions=12) - n_ref = len(ref_dirs) # 91 for n_partitions=12 - mixed_mating = MixedVariableMating( - eliminate_duplicates=MixedVariableDuplicateElimination()) - mixed_dedup = MixedVariableDuplicateElimination() - - # Resume from checkpoint or start fresh - resumed_gen = 0 - if args.resume: - resume_path = Path(args.resume) - if not resume_path.exists(): - console.print(f"[bold red]Checkpoint not found:[/] {resume_path}") - return - ckpt = load_checkpoint(resume_path) - ckpt_fmt = ckpt.get("format") - if ckpt_fmt not in ("mixed_v1", "mixed_v2"): - console.print("[bold red]Cannot resume from old-format checkpoint.[/]") - console.print("Old checkpoints used float arrays; new format uses mixed variables.") - console.print("Please start a fresh optimization run (remove --resume).") - return - if ckpt_fmt == "mixed_v1": - console.print("[bold yellow]Note:[/] resuming from v1 checkpoint. " - "Old shared vsm_amax/refine will be expanded to per-run arrays.") - if ckpt.get("max_runs") != max_runs: - console.print(f"[bold red]max_runs mismatch:[/] checkpoint has " - f"{ckpt.get('max_runs')}, requested {max_runs}") - return - ckpt_profile = ckpt.get("profile", "protein") - if ckpt_profile != _active_profile["seq_type_str"]: - console.print(f"[bold red]Profile mismatch:[/] checkpoint has " - f"'{ckpt_profile}', current dataset uses " - f"'{_active_profile['seq_type_str']}'") - return - pop_X = ckpt["pop_X"] - pop_F = ckpt["pop_F"] - pop_G = ckpt.get("pop_G") - pop_H = ckpt.get("pop_H") - resumed_gen = ckpt["n_gen_completed"] - problem.history = ckpt.get("history", []) - problem.eval_count = len(problem.history) - console.print(f"[bold green]Resumed[/] from generation {resumed_gen} " - f"({len(problem.history)} prior evaluations)") - remaining = args.n_gen - resumed_gen - if remaining <= 0: - console.print(f"[bold yellow]Already completed {resumed_gen} generations " - f"(requested {args.n_gen}). Increase --n-gen to continue.[/]") - return - termination = get_termination("n_gen", remaining) - # Reconstruct evaluated population so pymoo skips re-evaluation - pop = Population.new("X", pop_X) - pop.set("F", pop_F) - if pop_G is not None: - pop.set("G", pop_G) - if pop_H is not None: - pop.set("H", pop_H) - for ind in pop: - ind.evaluated = {"F", "G", "H"} - algorithm = NSGA3( - ref_dirs=ref_dirs, - pop_size=len(pop_X), - sampling=pop, - mating=mixed_mating, - eliminate_duplicates=mixed_dedup, - ) - else: - pop_size = args.pop_size - if pop_size < n_ref: - console.print(f"[bold yellow]Warning:[/] pop_size ({pop_size}) < reference " - f"directions ({n_ref}). Consider --pop-size {n_ref} or larger.") - - # Seed from another checkpoint (parameters only, fitness will be re-evaluated) - seed_sampling = MixedVariableSampling() - if args.seed_from: - seed_path = Path(args.seed_from) - if not seed_path.exists(): - console.print(f"[bold red]Seed checkpoint not found:[/] {seed_path}") - return - seed_ckpt = load_checkpoint(seed_path) - seed_X = seed_ckpt["pop_X"] - seed_beta = seed_ckpt.get("f_beta", 1.0) - console.print(f"[bold green]Seeding[/] initial population with {len(seed_X)} " - f"individuals from {seed_path.name} (was β={seed_beta})") - # Take up to pop_size individuals, sorted by original fitness (best first) - seed_F = seed_ckpt["pop_F"] - # Sort by first objective (best = most negative) - order = np.argsort(seed_F[:, 0]) - seed_X = seed_X[order][:pop_size] - - # Inject diversity for new binary variables: flip ~25% of - # ensemble individuals to consistency_merge=1 so the optimizer - # can compare both paths from the start. - n_flipped = 0 - for idx, x in enumerate(seed_X): - if not isinstance(x, dict): - continue - if int(x.get("n_runs", 1)) > 1 and idx % 4 == 0: - x["consistency_merge"] = 1 - n_flipped += 1 - if n_flipped: - console.print(f" Flipped {n_flipped} individuals to consistency_merge=1") - - # Create unevaluated population — pymoo will evaluate with new f_beta - seed_pop = Population.new("X", seed_X) - seed_sampling = seed_pop - - algorithm = NSGA3( - ref_dirs=ref_dirs, - pop_size=pop_size, - sampling=seed_sampling, - mating=mixed_mating, - eliminate_duplicates=mixed_dedup, - ) - termination = get_termination("n_gen", args.n_gen) - - if dashboard: - dashboard.start() - - try: - res = minimize( - problem, - algorithm, - termination, - seed=args.seed, - verbose=not use_dashboard, - callback=callback, - ) - except KeyboardInterrupt: - if dashboard: - dashboard.stop() - console.print("\n[bold yellow]Interrupted![/] Checkpoint was saved after last " - f"completed generation to:\n {checkpoint_path}") - console.print("Resume with: [bold]--resume " + str(checkpoint_path) + "[/]") - os._exit(1) # noqa: SLF001 - finally: - if dashboard: - dashboard.stop() - - # --- Results --- - console.print(f"\n[bold]Optimization complete.[/] " - f"{len(res.F)} Pareto-optimal solutions found.\n") - - pareto_configs = [] - for i, (x, f) in enumerate(zip(res.X, res.F)): - params = decode_unified_params(x, max_runs) - recall = -f[0] - precision = -f[1] - wt = f[2] - f1 = fbeta_score(precision, recall, 1.0) - pareto_configs.append({ - "params": params, "recall_cv": recall, "prec_cv": precision, - "f1_cv": f1, "wall_time": wt, - }) - - # Print Pareto front - table = Table(title="Pareto Front (sorted by CV F1)") - table.add_column("#", style="dim", width=3) - table.add_column("Mode", width=6) - table.add_column("Recall", justify="right") - table.add_column("Prec", justify="right") - table.add_column("F1", justify="right") - table.add_column("Time", justify="right") - table.add_column("Parameters") - - sorted_pareto = sorted(pareto_configs, key=lambda x: -x["f1_cv"]) - bl_best_recall = max(bl.get("recall", 0) for bl in baselines.values()) - bl_best_prec = max(bl.get("precision", 0) for bl in baselines.values()) - - for i, cfg in enumerate(sorted_pareto[:30]): - r_style = "bold green" if cfg.get("recall_cv", 0) > bl_best_recall else "" - p_style = "bold green" if cfg.get("prec_cv", 0) > bl_best_prec else "" - table.add_row( - str(i), - mode_label(cfg["params"]), - Text(f"{cfg.get('recall_cv', 0):.4f}", style=r_style), - Text(f"{cfg.get('prec_cv', 0):.4f}", style=p_style), - f"{cfg['f1_cv']:.4f}", - f"{cfg['wall_time']:.0f}s", - format_unified_short(cfg["params"]), - ) - console.print(table) - - # --- Mode summary --- - mode_counts: Dict[str, int] = defaultdict(int) - mode_best_f1: Dict[str, float] = defaultdict(float) - for cfg in pareto_configs: - m = mode_label(cfg["params"]) - mode_counts[m] += 1 - mode_best_f1[m] = max(mode_best_f1[m], cfg["f1_cv"]) - - console.print("\n[bold]Mode distribution on Pareto front:[/]") - for m in sorted(mode_counts.keys()): - console.print(f" {m}: {mode_counts[m]} solutions, best F1={mode_best_f1[m]:.4f}") - - # --- Re-evaluate top-3 on FULL dataset --- - console.print(f"\n[bold]Full-dataset evaluation[/] (top 3 by F1, checking for overfit)") - top3 = sorted_pareto[:3] - for i, cfg in enumerate(top3): - full_result = evaluate_unified(cfg["params"], cases, args.n_threads) - console.print(f"\n [{i}] {mode_label(cfg['params'])} " - f"CV R={cfg.get('recall_cv', 0):.4f} P={cfg.get('prec_cv', 0):.4f} " - f"F1={cfg['f1_cv']:.4f} -> Full F1={full_result['f1']:.4f}") - gap_f1 = full_result["f1"] - cfg["f1_cv"] - console.print(f" Overfit check: F1 {gap_f1:+.4f}") - for cat, v in sorted(full_result["per_category"].items()): - console.print(f" {cat}: F1={v['f1']:.4f} TC={v['tc']:.4f} (n={v['n']})") - console.print(f" {format_unified_long(cfg['params'])}") - - # --- Recommended tiers --- - console.print(f"\n{'='*60}") - console.print("[bold]Recommended configurations:[/]") - - # Fast: best F1 among solutions under 15s - fast_candidates = [c for c in sorted_pareto if c["wall_time"] < 15] - if fast_candidates: - best_fast = fast_candidates[0] - console.print(f"\n [bold]Fast[/] (< 15s): F1={best_fast['f1_cv']:.4f} " - f"R={best_fast.get('recall_cv', 0):.4f} P={best_fast.get('prec_cv', 0):.4f} " - f"Time={best_fast['wall_time']:.0f}s") - console.print(f" {format_unified_short(best_fast['params'])}") - - # Default: best F1 among solutions under 60s - default_candidates = [c for c in sorted_pareto if c["wall_time"] < 60] - if default_candidates: - best_default = default_candidates[0] - console.print(f"\n [bold]Default[/] (< 60s): F1={best_default['f1_cv']:.4f} " - f"R={best_default.get('recall_cv', 0):.4f} P={best_default.get('prec_cv', 0):.4f} " - f"Time={best_default['wall_time']:.0f}s") - console.print(f" {format_unified_short(best_default['params'])}") - - # Accurate: best F1 overall - best_overall = sorted_pareto[0] - console.print(f"\n [bold]Accurate[/] (best F1): F1={best_overall['f1_cv']:.4f} " - f"R={best_overall.get('recall_cv', 0):.4f} P={best_overall.get('prec_cv', 0):.4f} " - f"Time={best_overall['wall_time']:.0f}s") - console.print(f" {format_unified_short(best_overall['params'])}") - - # --- Save --- - results = { - "pareto_configs": pareto_configs, - "history": problem.history, - "baselines": baselines, - "folds_info": [(len(tr), len(te)) for tr, te in folds], - "args": vars(args), - "max_runs": max_runs, - "profile": _active_profile["seq_type_str"], - "dataset": args.dataset, - } - results_path = output_dir / "optim_results.pkl" - with open(results_path, "wb") as f: - pickle.dump(results, f) - console.print(f"\nResults saved to {results_path}") - - summary_path = output_dir / "pareto_front.txt" - with open(summary_path, "w") as f: - f.write(f"# Unified kalign optimization (NSGA-III, 3 objectives: recall, precision, time)\n") - f.write(f"# pop_size={args.pop_size} n_gen={args.n_gen} max_runs={max_runs} " - f"n_folds={k} seed={args.seed}\n") - f.write(f"# Baselines:\n") - for name, bl in baselines.items(): - f.write(f"# {name:10s} F1={bl['f1']:.4f} TC={bl['tc']:.4f} " - f"Time={bl['wall_time']:.0f}s\n") - f.write(f"\n") - - for i, cfg in enumerate(sorted_pareto): - p = cfg["params"] - f.write(f"[{i}] mode={mode_label(p)} " - f"R={cfg.get('recall_cv', cfg.get('f1_cv', 0)):.4f} " - f"P={cfg.get('prec_cv', cfg.get('tc_cv', 0)):.4f} " - f"F1={cfg.get('f1_cv', 0):.4f} " - f"Time={cfg['wall_time']:.0f}s\n") - f.write(f" n_runs={p['n_runs']}\n") - for run_k in range(p["n_runs"]): - mat = MATRIX_NAMES.get(p["run_types"][run_k], "?") - ref = REFINE_LONG.get(p["run_refine"][run_k], "?") - f.write(f" run_{run_k}: gpo={p['run_gpo'][run_k]:.3f} " - f"gpe={p['run_gpe'][run_k]:.3f} " - f"tgpe={p['run_tgpe'][run_k]:.3f} " - f"noise={p['run_noise'][run_k]:.3f} {mat} " - f"vsm={p['run_vsm_amax'][run_k]:.3f} ref={ref}\n") - f.write(f" seq_weights={p['seq_weights']:.3f}\n") - f.write(f" consistency={p['consistency']} " - f"consistency_weight={p['consistency_weight']:.3f}\n") - f.write(f" realign={p['realign']} " - f"min_support={p['min_support']}\n") - if p.get("consistency_merge", 0): - f.write(f" consistency_merge=1 " - f"consistency_merge_weight={p['consistency_merge_weight']:.3f}\n") - f.write("\n") - - console.print(f"Pareto front saved to {summary_path}") - - -if __name__ == "__main__": - main() diff --git a/benchmarks/run_balibase_comparison.py b/benchmarks/run_balibase_comparison.py deleted file mode 100644 index b422bb3..0000000 --- a/benchmarks/run_balibase_comparison.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Run BAliBASE quality benchmark across all 4 kalign mode presets.""" - -import json -from pathlib import Path - -import kalign -from benchmarks.datasets import get_cases, download_dataset -from benchmarks.scoring import run_case - -download_dataset("balibase") -cases = get_cases("balibase") -print(f"Loaded {len(cases)} BAliBASE cases") - -modes = ["fast", "default", "recall", "accurate"] - -all_results = {} - -for mode in modes: - print(f"\n{'=' * 60}") - print(f" mode={mode}") - print(f"{'=' * 60}") - - recalls, precs, f1s, tcs, times = [], [], [], [], [] - errors = 0 - for i, case in enumerate(cases): - r = run_case(case, method="python_api", n_threads=8, mode=mode) - if r.error: - errors += 1 - continue - recalls.append(r.recall) - precs.append(r.precision) - f1s.append(r.f1) - tcs.append(r.tc) - times.append(r.wall_time) - if (i + 1) % 50 == 0: - n = len(f1s) - print(f" [{i+1}/{len(cases)}] F1={sum(f1s)/n:.4f} so far...") - - n = len(f1s) - avg_r = sum(recalls) / n if n else 0 - avg_p = sum(precs) / n if n else 0 - avg_f = sum(f1s) / n if n else 0 - avg_t = sum(tcs) / n if n else 0 - total_t = sum(times) - - all_results[mode] = { - "mode": mode, - "n_cases": n, - "errors": errors, - "recall": round(avg_r, 4), - "precision": round(avg_p, 4), - "f1": round(avg_f, 4), - "tc": round(avg_t, 4), - "total_time": round(total_t, 1), - } - print(f" Recall={avg_r:.4f} Prec={avg_p:.4f} " - f"F1={avg_f:.4f} TC={avg_t:.4f} " - f"Time={total_t:.1f}s ({errors} errors)") - -# Summary -print() -print("=" * 72) -print(" RESULTS") -print("=" * 72) -print() -print(f" {'Mode':<12} {'N':>4} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8} {'Time':>8}") -print(f" {'-'*12} {'-'*4} {'-'*8} {'-'*8} {'-'*8} {'-'*8} {'-'*8}") -for mode in modes: - s = all_results[mode] - print(f" {s['mode']:<12} {s['n_cases']:>4} {s['recall']:>8.4f} {s['precision']:>8.4f} " - f"{s['f1']:>8.4f} {s['tc']:>8.4f} {s['total_time']:>7.1f}s") - -out_file = Path("benchmarks/results/balibase_modes.json") -out_file.parent.mkdir(exist_ok=True) -with open(out_file, "w") as f: - json.dump(all_results, f, indent=2) -print(f"\nSaved to {out_file}") diff --git a/benchmarks/view_pareto.py b/benchmarks/view_pareto.py deleted file mode 100644 index d4f4f3f..0000000 --- a/benchmarks/view_pareto.py +++ /dev/null @@ -1,1014 +0,0 @@ -#!/usr/bin/env python3 -"""Interactive Dash app to visualize the Pareto front from a unified optimizer checkpoint. - -Usage: - # View local checkpoint - uv run python -m benchmarks.view_pareto benchmarks/results/unified_optim/gen_checkpoint.pkl - - # Pull from server and view (auto-refresh every 30s) - uv run python -m benchmarks.view_pareto --remote tki-workstation:tmp/kalign35/benchmarks/results/unified_optim/gen_checkpoint.pkl - - # Custom port and refresh interval - uv run python -m benchmarks.view_pareto --port 8051 --refresh 60 checkpoint.pkl -""" - -import argparse -import json -import os -import pickle -import subprocess -import time -from pathlib import Path - -# Disable all Dash/Plotly telemetry before importing -os.environ["DASH_DISABLE_TELEMETRY"] = "1" -os.environ["PLOTLY_RENDERER"] = "browser" - -import numpy as np -import pandas as pd -import plotly.express as px -import plotly.graph_objects as go -from dash import Dash, Input, Output, State, ctx, dash_table, dcc, html - -try: - from kneed import KneeLocator -except ImportError: - KneeLocator = None - -from .optimize_unified import ( - MATRIX_NAMES, - REFINE_LONG, - decode_unified_params, - mode_label, -) - - -def load_checkpoint(path: str): - """Load checkpoint, optionally from remote via rsync.""" - with open(path, "rb") as f: - return pickle.load(f) # noqa: S301 - - -def sync_remote(remote_path: str, local_path: str): - """Pull checkpoint from remote server.""" - result = subprocess.run( - ["rsync", "-az", remote_path, local_path], - capture_output=True, text=True, - ) - return result.returncode == 0 - - -def build_pareto_df(ckpt: dict, max_runs: int) -> tuple[pd.DataFrame, dict]: - """Build a DataFrame from checkpoint population. - - Returns (DataFrame, params_by_idx) where params_by_idx maps idx -> full decoded params. - Supports mixed_v1 and mixed_v2 (dict-per-individual) checkpoints. - """ - if ckpt.get("format") not in ("mixed_v1", "mixed_v2"): - raise ValueError( - "Old-format checkpoint (float arrays). " - "Re-run the optimizer to generate a mixed_v1/v2 checkpoint." - ) - pop_X = ckpt["pop_X"] - pop_F = ckpt["pop_F"] - - rows = [] - params_by_idx = {} - for i in range(len(pop_X)): - params = decode_unified_params(pop_X[i], max_runs) - params_by_idx[i] = params - f1 = -pop_F[i, 0] - tc = -pop_F[i, 1] - wt = pop_F[i, 2] - - mode = mode_label(params) - - # Per-run details - run_details = [] - for k in range(params["n_runs"]): - mat = MATRIX_NAMES.get(params["run_types"][k], "?") - ref = REFINE_LONG.get(params["run_refine"][k], "?") - noise = params["run_noise"][k] - noise_str = f" n={noise:.2f}" if noise > 0 else "" - run_details.append( - f"R{k}: gpo={params['run_gpo'][k]:.2f} " - f"gpe={params['run_gpe'][k]:.2f} " - f"tgpe={params['run_tgpe'][k]:.2f}{noise_str} {mat} " - f"vsm={params['run_vsm_amax'][k]:.2f} ref={ref}" - ) - - # Summarize per-run refine/vsm for the table - refs = "/".join(REFINE_LONG.get(r, "?") for r in params["run_refine"]) - - # Per-run summaries for hover - vsm_summary = "/".join(f"{v:.1f}" for v in params["run_vsm_amax"]) - mat_summary = "/".join( - MATRIX_NAMES.get(params["run_types"][k], "?") - for k in range(params["n_runs"]) - ) - - rows.append({ - "idx": i, - "f1": round(f1, 4), - "tc": round(tc, 4), - "wall_time": round(wt, 1), - "mode": mode, - "n_runs": params["n_runs"], - "vsm": vsm_summary, - "refine": refs, - "matrices": mat_summary, - "seq_weights": round(params["seq_weights"], 3), - "consistency": params["consistency"], - "consistency_weight": round(params["consistency_weight"], 3), - "realign": params["realign"], - "min_support": params["min_support"], - "run_details": "\n".join(run_details), - # Keep run-0 values for coloring - "vsm_amax_0": round(params["run_vsm_amax"][0], 3), - "gpo_0": round(params["run_gpo"][0], 2), - "gpe_0": round(params["run_gpe"][0], 2), - "tgpe_0": round(params["run_tgpe"][0], 2), - "matrix_0": MATRIX_NAMES.get(params["run_types"][0], "?"), - }) - - return pd.DataFrame(rows), params_by_idx - - -def build_history_df(ckpt: dict, max_runs: int) -> pd.DataFrame: - """Build DataFrame from evaluation history.""" - history = ckpt.get("history", []) - if not history: - return pd.DataFrame() - - rows = [] - for h in history: - cv = h.get("cv_result", {}) - p = h.get("params", {}) - rows.append({ - "eval": h.get("eval", 0), - "f1": cv.get("f1", 0), - "tc": cv.get("tc", 0), - "wall_time": cv.get("wall_time", 0), - "mode": mode_label(p) if p else "?", - }) - return pd.DataFrame(rows) - - -def _compute_pareto_2d(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: - """Extract the 2D Pareto front from a DataFrame. - - Assumes we want to maximize y_col and minimize x_col (if x is time) - or maximize both (if both are scores). - Returns points sorted by x_col for drawing as a line. - """ - if df.empty: - return df - - # Determine direction: for wall_time we minimize, for f1/tc we maximize - # The Pareto front is the "upper-left" boundary (max y, min x) - # or "upper-right" if both axes are scores to maximize - x_minimize = (x_col == "wall_time") - y_minimize = (y_col == "wall_time") - - points = df[[x_col, y_col]].values - n = len(points) - is_pareto = [True] * n - - for i in range(n): - if not is_pareto[i]: - continue - for j in range(n): - if i == j or not is_pareto[j]: - continue - # Check if j dominates i - xi, yi = points[i] - xj, yj = points[j] - - # "Better" depends on direction - xj_better = (xj < xi) if x_minimize else (xj > xi) - xj_equal = abs(xj - xi) < 1e-10 - yj_better = (yj < yi) if y_minimize else (yj > yi) - yj_equal = abs(yj - yi) < 1e-10 - - if (xj_better or xj_equal) and (yj_better or yj_equal) and (xj_better or yj_better): - is_pareto[i] = False - break - - pareto = df[is_pareto].copy() - pareto = pareto.sort_values(x_col) - return pareto - - -def _compute_pareto_3d(df: pd.DataFrame) -> pd.DataFrame: - """Extract the 3D Pareto front: maximize F1, maximize TC, minimize wall_time. - - A point is non-dominated if no other point is better in all three objectives. - """ - if df.empty: - return df - - n = len(df) - f1 = df["f1"].values - tc = df["tc"].values - wt = df["wall_time"].values - is_pareto = [True] * n - - for i in range(n): - if not is_pareto[i]: - continue - for j in range(n): - if i == j or not is_pareto[j]: - continue - # j dominates i if: f1_j >= f1_i AND tc_j >= tc_i AND wt_j <= wt_i - # with at least one strict inequality - if (f1[j] >= f1[i] and tc[j] >= tc[i] and wt[j] <= wt[i] and - (f1[j] > f1[i] or tc[j] > tc[i] or wt[j] < wt[i])): - is_pareto[i] = False - break - - return df[is_pareto].copy() - - -def auto_select_tiers(pareto_df: pd.DataFrame) -> dict: - """Auto-select fast/default/accurate tiers from the 2D Pareto front (F1 vs time). - - Uses the kneedle algorithm to find knee points on the F1-vs-time Pareto curve. - The knee point is where spending more time gives diminishing F1 returns. - - Returns dict with keys "fast", "default", "accurate", each containing - the DataFrame row index (idx column) or None if not enough data. - """ - if pareto_df.empty or len(pareto_df) < 3: - return {"fast": None, "default": None, "accurate": None} - - # Get the 2D Pareto front: maximize F1, minimize time - front = _compute_pareto_2d(pareto_df, "wall_time", "f1") - if len(front) < 3: - # Not enough points for knee detection - front_sorted = front.sort_values("wall_time") - return { - "fast": int(front_sorted.iloc[0]["idx"]), - "default": int(front_sorted.iloc[len(front_sorted) // 2]["idx"]), - "accurate": int(front_sorted.iloc[-1]["idx"]), - } - - # Sort by time (ascending) for kneedle - front = front.sort_values("wall_time").reset_index(drop=True) - x = front["wall_time"].values - y = front["f1"].values - - # Accurate: best F1 on the front - accurate_iloc = int(np.argmax(y)) - accurate_idx = int(front.iloc[accurate_iloc]["idx"]) - - # Fast: fastest Pareto-optimal point (lowest time, already sorted) - fast_iloc = 0 - fast_idx = int(front.iloc[0]["idx"]) - - # Default: use kneedle to find the knee point (diminishing returns) - default_idx = None - if KneeLocator is not None and len(x) >= 3: - try: - kn = KneeLocator( - x, y, - curve="concave", - direction="increasing", - S=1.0, - ) - if kn.knee is not None: - # Find the closest front point to the knee - knee_iloc = int(np.argmin(np.abs(x - kn.knee))) - default_idx = int(front.iloc[knee_iloc]["idx"]) - except Exception: - pass - - # Fallback: pick the point closest to 2/3 of the time range - if default_idx is None: - target_time = x[0] + 0.5 * (x[-1] - x[0]) - default_iloc = int(np.argmin(np.abs(x - target_time))) - default_idx = int(front.iloc[default_iloc]["idx"]) - - # Ensure fast < default < accurate in time and all different - # Find the iloc positions for ordering checks - default_iloc = int(front[front["idx"] == default_idx].index[0]) if default_idx is not None else None - if default_iloc is not None: - if default_iloc <= fast_iloc and fast_iloc + 1 < len(front): - default_iloc = fast_iloc + 1 - default_idx = int(front.iloc[default_iloc]["idx"]) - if default_iloc >= accurate_iloc and accurate_iloc > 0: - default_iloc = accurate_iloc - 1 - default_idx = int(front.iloc[default_iloc]["idx"]) - if default_idx == fast_idx: - if fast_iloc + 1 < len(front): - default_idx = int(front.iloc[fast_iloc + 1]["idx"]) - if default_idx == accurate_idx: - if accurate_iloc > 0: - default_idx = int(front.iloc[accurate_iloc - 1]["idx"]) - - return {"fast": fast_idx, "default": default_idx, "accurate": accurate_idx} - - -def _format_tier_config(row) -> str: - """Format a tier selection as copy-pasteable Python code.""" - lines = [ - f"# F1={row['f1']:.4f} TC={row['tc']:.4f} Time={row['wall_time']:.1f}s", - f"# Mode: {row['mode']}", - ] - # Per-run params - if row.get("run_details"): - for line in row["run_details"].split("\n"): - lines.append(f"# {line}") - lines.append(f"config = {{") - lines.append(f' "n_runs": {row["n_runs"]},') - lines.append(f' "vsm": "{row["vsm"]}", # per-run') - lines.append(f' "seq_weights": {row["seq_weights"]},') - lines.append(f' "consistency": {row["consistency"]},') - lines.append(f' "consistency_weight": {row["consistency_weight"]},') - lines.append(f' "realign": {row["realign"]},') - lines.append(f' "refine": "{row["refine"]}",') - lines.append(f' "min_support": {row["min_support"]},') - lines.append(f"}}") - return "\n".join(lines) - - -def create_app(ckpt_path: str, remote_path: str = "", refresh_sec: int = 30, - max_runs: int = 5): - app = Dash(__name__, title="Kalign Pareto Front") - - app.layout = html.Div([ - dcc.Store(id="ckpt-path", data=ckpt_path), - dcc.Store(id="remote-path", data=remote_path or ""), - dcc.Store(id="max-runs", data=max_runs), - dcc.Interval(id="refresh-interval", interval=refresh_sec * 1000, - disabled=(not remote_path)), - - html.H2("Kalign Unified Optimizer - Pareto Front"), - html.Div(id="status-bar", style={"marginBottom": "10px", "color": "#666"}), - - # Controls row - html.Div([ - html.Div([ - html.Label("Color by:"), - dcc.Dropdown( - id="color-by", - options=[ - {"label": "Mode", "value": "mode"}, - {"label": "VSM amax (R0)", "value": "vsm_amax_0"}, - {"label": "Seq weights", "value": "seq_weights"}, - {"label": "Consistency", "value": "consistency"}, - {"label": "Realign", "value": "realign"}, - {"label": "Refine", "value": "refine"}, - {"label": "Matrix (R0)", "value": "matrix_0"}, - {"label": "GPO (R0)", "value": "gpo_0"}, - ], - value="mode", - clearable=False, - ), - ], style={"width": "200px", "display": "inline-block", "marginRight": "20px"}), - html.Div([ - html.Label("X axis:"), - dcc.Dropdown( - id="x-axis", - options=[ - {"label": "Wall time (s)", "value": "wall_time"}, - {"label": "F1", "value": "f1"}, - {"label": "TC", "value": "tc"}, - ], - value="wall_time", - clearable=False, - ), - ], style={"width": "200px", "display": "inline-block", "marginRight": "20px"}), - html.Div([ - html.Label("Y axis:"), - dcc.Dropdown( - id="y-axis", - options=[ - {"label": "F1", "value": "f1"}, - {"label": "TC", "value": "tc"}, - {"label": "Wall time (s)", "value": "wall_time"}, - ], - value="f1", - clearable=False, - ), - ], style={"width": "200px", "display": "inline-block", "marginRight": "20px"}), - html.Div([ - html.Label("Filter mode:"), - dcc.Checklist( - id="mode-filter", - options=[ - {"label": "single", "value": "single"}, - {"label": "ens3", "value": "ens3"}, - {"label": "ens5", "value": "ens5"}, - ], - value=["single", "ens3", "ens5"], - inline=True, - ), - ], style={"display": "inline-block"}), - ], style={"marginBottom": "10px"}), - - # Main scatter plot - dcc.Graph(id="pareto-scatter", style={"height": "600px"}), - - # --- Tier Selection --- - html.H3("Default Configuration Selection"), - html.P("Auto-suggested tiers using knee-point detection on the F1-vs-time " - "Pareto front. Click any point on the 2D scatter plot, then assign " - "it to a tier with the buttons below.", - style={"color": "#666", "fontSize": "14px"}), - - html.Div([ - html.Div([ - html.Button("Set as Fast", id="set-fast-btn", n_clicks=0, - style={"backgroundColor": "#2196F3", "color": "white", - "border": "none", "padding": "8px 16px", - "marginRight": "10px", "cursor": "pointer"}), - html.Button("Set as Default", id="set-default-btn", n_clicks=0, - style={"backgroundColor": "#FF9800", "color": "white", - "border": "none", "padding": "8px 16px", - "marginRight": "10px", "cursor": "pointer"}), - html.Button("Set as Accurate", id="set-accurate-btn", n_clicks=0, - style={"backgroundColor": "#4CAF50", "color": "white", - "border": "none", "padding": "8px 16px", - "marginRight": "10px", "cursor": "pointer"}), - html.Button("Auto-select (kneedle)", id="auto-select-btn", n_clicks=0, - style={"backgroundColor": "#9E9E9E", "color": "white", - "border": "none", "padding": "8px 16px", - "cursor": "pointer"}), - ], style={"marginBottom": "10px"}), - ]), - - # Individual stores for each tier (avoids output=state on same component) - dcc.Store(id="tier-fast", data=None), - dcc.Store(id="tier-default", data=None), - dcc.Store(id="tier-accurate", data=None), - # Store for last clicked point idx - dcc.Store(id="last-clicked-idx", data=None), - - html.Div(id="tier-display", style={"marginBottom": "20px"}), - - html.Div([ - html.Button("Save tiers to JSON", id="save-tiers-btn", n_clicks=0, - style={"backgroundColor": "#673AB7", "color": "white", - "border": "none", "padding": "8px 16px", - "cursor": "pointer", "marginRight": "10px"}), - html.Span(id="save-status", style={"color": "#666", "fontSize": "14px"}), - ], style={"marginBottom": "20px"}), - - # 3D scatter - html.H3("3D Pareto Surface (F1 vs TC vs Time)"), - dcc.Graph(id="pareto-3d", style={"height": "600px"}), - - # Convergence plot - html.H3("Convergence (best F1 / TC over evaluations)"), - dcc.Graph(id="convergence-plot", style={"height": "350px"}), - - # Selected point details - html.H3("Click a point to see full parameters:"), - html.Pre(id="point-details", - style={"backgroundColor": "#f5f5f5", "padding": "15px", - "fontSize": "14px", "whiteSpace": "pre-wrap"}), - - # Top solutions table - html.H3("3D Pareto Front (non-dominated in F1, TC, and time)"), - dash_table.DataTable( - id="top-table", - columns=[ - {"name": "#", "id": "idx"}, - {"name": "Mode", "id": "mode"}, - {"name": "F1", "id": "f1"}, - {"name": "TC", "id": "tc"}, - {"name": "Time", "id": "wall_time"}, - {"name": "VSM", "id": "vsm"}, - {"name": "SW", "id": "seq_weights"}, - {"name": "C", "id": "consistency"}, - {"name": "CW", "id": "consistency_weight"}, - {"name": "Re", "id": "realign"}, - {"name": "Ref", "id": "refine"}, - {"name": "MS", "id": "min_support"}, - {"name": "GPO(R0)", "id": "gpo_0"}, - {"name": "GPE(R0)", "id": "gpe_0"}, - {"name": "TGPE(R0)", "id": "tgpe_0"}, - {"name": "Mat(R0)", "id": "matrix_0"}, - ], - style_cell={"textAlign": "right", "padding": "4px", "fontSize": "13px"}, - style_header={"fontWeight": "bold"}, - style_data_conditional=[ - {"if": {"column_id": "mode"}, "textAlign": "left"}, - {"if": {"column_id": "refine"}, "textAlign": "center"}, - ], - sort_action="native", - row_selectable="single", - page_size=50, - ), - ], style={"maxWidth": "1400px", "margin": "auto", "padding": "20px"}) - - # Store for current data; params_by_idx maps idx -> full decoded params dict - app._df_cache = {"df": None, "hist_df": None, "mtime": 0, "params_by_idx": {}} # type: ignore[attr-defined] - - def _load_data(ckpt_path_arg, remote_path_arg, max_runs_arg): - """Load or refresh data from checkpoint.""" - local_path = ckpt_path_arg - - # Sync from remote if configured - if remote_path_arg: - sync_remote(remote_path_arg, local_path) - - if not Path(local_path).exists(): - return None, None, 0 - - mtime = Path(local_path).stat().st_mtime - if mtime == app._df_cache["mtime"]: # type: ignore[attr-defined] - return app._df_cache["df"], app._df_cache["hist_df"], mtime # type: ignore[attr-defined] - - try: - ckpt = load_checkpoint(local_path) - except Exception: - return app._df_cache["df"], app._df_cache["hist_df"], app._df_cache["mtime"] # type: ignore[attr-defined] - - mr = ckpt.get("max_runs", max_runs_arg) - df, params_by_idx = build_pareto_df(ckpt, mr) - hist_df = build_history_df(ckpt, mr) - n_gen = ckpt.get("n_gen_completed", "?") - - f_beta = ckpt.get("f_beta", 1.0) - app._df_cache = {"df": df, "hist_df": hist_df, "mtime": mtime, "n_gen": n_gen, "params_by_idx": params_by_idx, "f_beta": f_beta} # type: ignore[attr-defined] - return df, hist_df, mtime - - @app.callback( - Output("pareto-scatter", "figure"), - Output("pareto-3d", "figure"), - Output("convergence-plot", "figure"), - Output("top-table", "data"), - Output("status-bar", "children"), - Input("refresh-interval", "n_intervals"), - Input("color-by", "value"), - Input("x-axis", "value"), - Input("y-axis", "value"), - Input("mode-filter", "value"), - Input("tier-fast", "data"), - Input("tier-default", "data"), - Input("tier-accurate", "data"), - Input("ckpt-path", "data"), - Input("remote-path", "data"), - Input("max-runs", "data"), - ) - def update_all(n_intervals, color_by, x_axis, y_axis, mode_filter, - tier_fast, tier_default, tier_accurate, - ckpt_path, remote_path, max_runs): - tier_selections = {"fast": tier_fast, "default": tier_default, "accurate": tier_accurate} - df, hist_df, mtime = _load_data(ckpt_path, remote_path or None, max_runs) - - empty_fig = go.Figure() - if df is None or df.empty: - return empty_fig, empty_fig, empty_fig, [], "No data loaded" - - # Filter by mode - filtered = df[df["mode"].isin(mode_filter)] if mode_filter else df - - # Status - n_gen = app._df_cache.get("n_gen", "?") - f_beta = app._df_cache.get("f_beta", 1.0) - obj_label = f"F{f_beta}" if f_beta != 1.0 else "F1" - ago = time.time() - mtime if mtime else 0 - status = (f"Generation {n_gen} | {len(df)} individuals | " - f"Best {obj_label}={df['f1'].max():.4f} | Best TC={df['tc'].max():.4f} | " - f"Last update: {ago:.0f}s ago") - - # 2D scatter - hover_data = ["mode", "f1", "tc", "wall_time", - "n_runs", "vsm", "refine", "matrices", - "seq_weights", "consistency", "realign", "min_support"] - fig2d = px.scatter( - filtered, x=x_axis, y=y_axis, color=color_by, - hover_data=hover_data, - title=f"Population ({y_axis} vs {x_axis})", - template="plotly_white", - ) - fig2d.update_traces(marker=dict(size=8, opacity=0.7)) - - # Compute and draw overall 2D Pareto front line - pareto_line = _compute_pareto_2d(filtered, x_axis, y_axis) - if len(pareto_line) > 1: - fig2d.add_trace(go.Scatter( - x=pareto_line[x_axis].tolist(), - y=pareto_line[y_axis].tolist(), - mode="lines+markers", - name="Pareto front (all)", - line=dict(color="rgba(0,0,0,0.7)", width=2.5), - marker=dict(size=10, symbol="diamond", color="rgba(0,0,0,0.7)"), - hovertext=[ - f"F1={r['f1']:.4f} TC={r['tc']:.4f} t={r['wall_time']:.0f}s " - f"{r['mode']}" - for _, r in pareto_line.iterrows() - ], - hoverinfo="text", - )) - - # Per-mode Pareto front lines - mode_colors = {"single": "rgba(31,119,180,0.6)", - "ens3": "rgba(255,127,14,0.6)", - "ens5": "rgba(44,160,44,0.6)"} - for mode_name in sorted(filtered["mode"].unique()): - mode_df = filtered[filtered["mode"] == mode_name] - mode_pareto = _compute_pareto_2d(mode_df, x_axis, y_axis) - if len(mode_pareto) > 1: - fig2d.add_trace(go.Scatter( - x=mode_pareto[x_axis].tolist(), - y=mode_pareto[y_axis].tolist(), - mode="lines", - name=f"Pareto ({mode_name})", - line=dict(color=mode_colors.get(mode_name, "rgba(128,128,128,0.5)"), - width=1.5, dash="dash"), - hoverinfo="skip", - showlegend=True, - )) - - # Tier star markers - if tier_selections: - tier_styles = [ - ("fast", "Fast", "#2196F3"), - ("default", "Default", "#FF9800"), - ("accurate", "Accurate", "#4CAF50"), - ] - for key, label, color in tier_styles: - idx = tier_selections.get(key) - if idx is None: - continue - match = filtered[filtered["idx"] == idx] - if match.empty: - match = df[df["idx"] == idx] - if match.empty: - continue - r = match.iloc[0] - fig2d.add_trace(go.Scatter( - x=[r[x_axis]], y=[r[y_axis]], - mode="markers+text", - name=f"★ {label}", - marker=dict(size=18, symbol="star", color=color, - line=dict(width=2, color="black")), - text=[label], textposition="top center", - textfont=dict(size=12, color=color), - hovertext=f"{label}: F1={r['f1']:.4f} TC={r['tc']:.4f} t={r['wall_time']:.0f}s", - hoverinfo="text", - )) - - fig2d.update_layout(height=600) - - # 3D scatter - fig3d = px.scatter_3d( - filtered, x="wall_time", y="f1", z="tc", color=color_by, - hover_data=hover_data, - title="3D Pareto Surface", - template="plotly_white", - ) - fig3d.update_traces(marker=dict(size=5, opacity=0.7)) - fig3d.update_layout(height=600, scene=dict( - xaxis_title="Wall time (s)", - yaxis_title="F1", - zaxis_title="TC", - )) - - # Convergence - if hist_df is not None and not hist_df.empty: - hist_df = hist_df.copy() - hist_df["best_f1"] = hist_df["f1"].cummax() - hist_df["best_tc"] = hist_df["tc"].cummax() - fig_conv = go.Figure() - fig_conv.add_trace(go.Scatter( - x=hist_df["eval"], y=hist_df["best_f1"], - mode="lines", name="Best F1", - )) - fig_conv.add_trace(go.Scatter( - x=hist_df["eval"], y=hist_df["best_tc"], - mode="lines", name="Best TC", - )) - fig_conv.update_layout( - template="plotly_white", height=350, - xaxis_title="Evaluation #", yaxis_title="Score", - ) - else: - fig_conv = empty_fig - - # Pareto front table (3D: maximize F1, maximize TC, minimize time) - pareto_3d = _compute_pareto_3d(filtered) - pareto_3d = pareto_3d.sort_values("f1", ascending=False) - table_data = pareto_3d.drop(columns=["run_details"]).to_dict("records") - - return fig2d, fig3d, fig_conv, table_data, status - - @app.callback( - Output("last-clicked-idx", "data"), - Input("pareto-scatter", "clickData"), - State("mode-filter", "value"), - State("x-axis", "value"), - State("y-axis", "value"), - ) - def track_click(click_data, mode_filter, x_axis, y_axis): - """Track the idx of the last clicked point on the 2D scatter.""" - df = app._df_cache.get("df") - if df is None or click_data is None: - return None - pts = click_data.get("points", []) - if not pts: - return None - pt = pts[0] - x_val = pt.get("x") - y_val = pt.get("y") - if x_val is not None and y_val is not None: - filtered = df[df["mode"].isin(mode_filter)] if mode_filter else df - if filtered.empty: - return None - # Normalize distances by range to handle different scales - x_range = filtered[x_axis].max() - filtered[x_axis].min() - y_range = filtered[y_axis].max() - filtered[y_axis].min() - x_range = max(x_range, 1e-10) - y_range = max(y_range, 1e-10) - dists = ((filtered[x_axis] - x_val) / x_range)**2 + \ - ((filtered[y_axis] - y_val) / y_range)**2 - best = dists.idxmin() - return int(filtered.loc[best, "idx"]) - return None - - @app.callback( - Output("tier-fast", "data"), - Output("tier-default", "data"), - Output("tier-accurate", "data"), - Input("set-fast-btn", "n_clicks"), - Input("set-default-btn", "n_clicks"), - Input("set-accurate-btn", "n_clicks"), - Input("auto-select-btn", "n_clicks"), - State("tier-fast", "data"), - State("tier-default", "data"), - State("tier-accurate", "data"), - State("last-clicked-idx", "data"), - State("mode-filter", "value"), - ) - def update_tiers(_fc, _dc, _ac, _auto, - cur_fast, cur_default, cur_accurate, last_idx, mode_filter): - """Update tier selections based on button clicks.""" - triggered_id = ctx.triggered_id - if triggered_id is None: - return cur_fast, cur_default, cur_accurate - - if triggered_id == "auto-select-btn": - df = app._df_cache.get("df") - if df is not None and not df.empty: - filtered = df[df["mode"].isin(mode_filter)] if mode_filter else df - tiers = auto_select_tiers(filtered) - return tiers.get("fast"), tiers.get("default"), tiers.get("accurate") - return cur_fast, cur_default, cur_accurate - - if last_idx is None: - return cur_fast, cur_default, cur_accurate - - if triggered_id == "set-fast-btn": - return last_idx, cur_default, cur_accurate - elif triggered_id == "set-default-btn": - return cur_fast, last_idx, cur_accurate - elif triggered_id == "set-accurate-btn": - return cur_fast, cur_default, last_idx - - return cur_fast, cur_default, cur_accurate - - @app.callback( - Output("tier-display", "children"), - Input("tier-fast", "data"), - Input("tier-default", "data"), - Input("tier-accurate", "data"), - ) - def render_tiers(tier_fast, tier_default, tier_accurate): - """Render the selected tier configurations.""" - df = app._df_cache.get("df") - tiers = {"fast": tier_fast, "default": tier_default, "accurate": tier_accurate} - if df is None: - return html.Div("No tiers selected yet.", style={"color": "#999"}) - - tier_names = [("fast", "Fast", "#2196F3"), - ("default", "Default", "#FF9800"), - ("accurate", "Accurate", "#4CAF50")] - cards = [] - - for key, label, color in tier_names: - idx = tiers.get(key) - if idx is None: - cards.append(html.Div([ - html.H4(f"{label}", style={"color": color, "marginBottom": "5px"}), - html.Span("Not set — click a point then press the button", - style={"color": "#999", "fontSize": "13px"}), - ], style={"display": "inline-block", "verticalAlign": "top", - "width": "32%", "marginRight": "1%", - "padding": "10px", "backgroundColor": "#fafafa", - "border": f"2px solid {color}", "borderRadius": "8px"})) - continue - - match = df[df["idx"] == idx] - if match.empty: - continue - row = match.iloc[0] - config_text = _format_tier_config(row) - cards.append(html.Div([ - html.H4(f"{label}", style={"color": color, "marginBottom": "5px"}), - html.Div(f"F1={row['f1']:.4f} TC={row['tc']:.4f} Time={row['wall_time']:.1f}s", - style={"fontWeight": "bold", "marginBottom": "5px"}), - html.Div(f"{row['mode']}", style={"fontSize": "13px", "marginBottom": "8px"}), - html.Pre(config_text, - style={"backgroundColor": "#f0f0f0", "padding": "8px", - "fontSize": "12px", "whiteSpace": "pre-wrap", - "margin": "0", "borderRadius": "4px"}), - ], style={"display": "inline-block", "verticalAlign": "top", - "width": "32%", "marginRight": "1%", - "padding": "10px", "backgroundColor": "#fafafa", - "border": f"2px solid {color}", "borderRadius": "8px"})) - - return html.Div(cards, style={"marginBottom": "20px"}) - - @app.callback( - Output("save-status", "children"), - Input("save-tiers-btn", "n_clicks"), - State("tier-fast", "data"), - State("tier-default", "data"), - State("tier-accurate", "data"), - State("ckpt-path", "data"), - ) - def save_tiers(n_clicks, tier_fast, tier_default, tier_accurate, ckpt_path): - """Save selected tiers to a JSON file next to the checkpoint.""" - tiers = {"fast": tier_fast, "default": tier_default, "accurate": tier_accurate} - if not n_clicks: - return "" - - df = app._df_cache.get("df") - params_by_idx = app._df_cache.get("params_by_idx", {}) - if df is None: - return "No data loaded." - - any_set = any(tiers.get(k) is not None for k in ("fast", "default", "accurate")) - if not any_set: - return "No tiers selected yet." - - output = {} - for tier_name in ("fast", "default", "accurate"): - idx = tiers.get(tier_name) - if idx is None: - continue - match = df[df["idx"] == idx] - if match.empty: - continue - row = match.iloc[0] - - # Full decoded params from the optimizer - full_params = params_by_idx.get(idx, {}) - - # Build per-run config list - runs = [] - for k in range(int(row["n_runs"])): - run = { - "gpo": round(float(full_params["run_gpo"][k]), 4), - "gpe": round(float(full_params["run_gpe"][k]), 4), - "tgpe": round(float(full_params["run_tgpe"][k]), 4), - "matrix": MATRIX_NAMES.get(full_params["run_types"][k], "?"), - "vsm_amax": round(float(full_params["run_vsm_amax"][k]), 4), - "refine": REFINE_LONG.get(full_params["run_refine"][k], "NONE"), - } - if full_params["run_noise"][k] > 0: - run["noise"] = round(float(full_params["run_noise"][k]), 4) - runs.append(run) - - output[tier_name] = { - "scores": { - "f1": float(row["f1"]), - "tc": float(row["tc"]), - "wall_time": float(row["wall_time"]), - }, - "params": { - "n_runs": int(row["n_runs"]), - "seq_weights": float(row["seq_weights"]), - "consistency": int(row["consistency"]), - "consistency_weight": float(row["consistency_weight"]), - "realign": int(row["realign"]), - "min_support": int(row["min_support"]), - }, - "runs": runs, - } - - # Save next to checkpoint file - out_path = Path(ckpt_path).parent / "kalign_tiers.json" - with open(out_path, "w") as f: - json.dump(output, f, indent=2) - - n_tiers = len(output) - return f"Saved {n_tiers} tier(s) to {out_path}" - - @app.callback( - Output("point-details", "children"), - Input("pareto-scatter", "clickData"), - Input("pareto-3d", "clickData"), - Input("top-table", "selected_rows"), - State("top-table", "data"), - State("x-axis", "value"), - State("y-axis", "value"), - ) - def show_details(click_2d, click_3d, selected_rows, table_data, - x_axis, y_axis): - df = app._df_cache.get("df") - if df is None or df.empty: - return "Click a point or select a table row to see details." - - triggered_id = ctx.triggered_id - - # From table selection - if triggered_id == "top-table" and selected_rows and table_data: - row = table_data[selected_rows[0]] - idx = row["idx"] - match = df[df["idx"] == idx] - if not match.empty: - return _format_detail(match.iloc[0]) - - # From scatter click — match by coordinates - click = None - if triggered_id == "pareto-3d" and click_3d: - click = click_3d - elif triggered_id == "pareto-scatter" and click_2d: - click = click_2d - - if click and click.get("points"): - pt = click["points"][0] - x_val = pt.get("x") - y_val = pt.get("y") - if x_val is not None and y_val is not None: - # For 3D clicks, match on wall_time and f1 - if triggered_id == "pareto-3d": - x_col, y_col = "wall_time", "f1" - else: - x_col, y_col = x_axis, y_axis - x_range = max(df[x_col].max() - df[x_col].min(), 1e-10) - y_range = max(df[y_col].max() - df[y_col].min(), 1e-10) - dists = ((df[x_col] - x_val) / x_range)**2 + \ - ((df[y_col] - y_val) / y_range)**2 - best = dists.idxmin() - return _format_detail(df.loc[best]) - - return "Click a point or select a table row to see details." - - return app - - -def _format_detail(row): - """Format full details for a selected solution.""" - lines = [ - f"Solution #{row['idx']}", - f"{'='*50}", - f"F1 = {row['f1']:.4f} TC = {row['tc']:.4f} Time = {row['wall_time']:.1f}s", - f"Mode: {row['mode']} n_runs={row['n_runs']}", - f"", - f"Per-run parameters:", - row["run_details"], - f"", - f"Shared parameters:", - f" seq_weights = {row['seq_weights']}", - f" consistency = {row['consistency']}", - f" consistency_wt = {row['consistency_weight']}", - f" realign = {row['realign']}", - f" min_support = {row['min_support']}", - f" (vsm_amax, refine are per-run — see run details above)", - ] - return "\n".join(lines) - - -def main(): - parser = argparse.ArgumentParser( - description="Interactive Pareto front viewer for kalign optimizer") - parser.add_argument("checkpoint", help="Path to gen_checkpoint.pkl (local)") - parser.add_argument("--remote", default=None, - help="Remote path (e.g. server:path/gen_checkpoint.pkl) to auto-sync") - parser.add_argument("--port", type=int, default=8050, - help="Dash server port (default: 8050)") - parser.add_argument("--refresh", type=int, default=30, - help="Auto-refresh interval in seconds (default: 30, remote only)") - parser.add_argument("--max-runs", type=int, default=5, - help="Max ensemble runs (must match optimizer, default: 5)") - args = parser.parse_args() - - # If remote specified, do initial sync - if args.remote: - print(f"Syncing from {args.remote}...") - sync_remote(args.remote, args.checkpoint) - - if not Path(args.checkpoint).exists(): - print(f"Checkpoint not found: {args.checkpoint}") - return - - app = create_app( - ckpt_path=args.checkpoint, - remote_path=args.remote or "", - refresh_sec=args.refresh, - max_runs=args.max_runs, - ) - - print(f"Starting Pareto viewer on http://localhost:{args.port}") - if args.remote: - print(f"Auto-refreshing from {args.remote} every {args.refresh}s") - app.run(debug=False, port=args.port) - - -if __name__ == "__main__": - main() diff --git a/benchmarks/vsm_ensemble_experiment.py b/benchmarks/vsm_ensemble_experiment.py deleted file mode 100644 index 35ecf7c..0000000 --- a/benchmarks/vsm_ensemble_experiment.py +++ /dev/null @@ -1,307 +0,0 @@ -"""Comprehensive BAliBASE benchmark comparing all kalign modes. - -Modes: - 1. baseline - no VSM, no refinement - 2. +vsm - VSM only (vsm_amax=2.0) - 3. +vsm+ref - VSM + refinement - 4. ens3 - ensemble(3), no VSM, no refinement in runs - 5. ens3+vsm - ensemble(3), VSM in each run - 6. ens3+vsm+ref - ensemble(3), VSM + refinement in each run - -Usage: - uv run python -m benchmarks.vsm_ensemble_experiment - uv run python -m benchmarks.vsm_ensemble_experiment --max-cases 10 - uv run python -m benchmarks.vsm_ensemble_experiment --categories RV11 -""" - -import argparse -import json -import statistics -import tempfile -import time -from collections import defaultdict -from pathlib import Path - -import kalign -from .datasets import get_cases -from .scoring import parse_balibase_xml - - -def _read_fasta_seqs(path): - seqs = [] - name = None - buf = [] - with open(path) as f: - for line in f: - line = line.strip() - if not line: - continue - if line.startswith(">"): - if name is not None: - seqs.append((name, "".join(buf))) - name = line[1:].split()[0] - buf = [] - else: - buf.append(line) - if name is not None: - seqs.append((name, "".join(buf))) - return seqs - - -def _alignment_stats(path): - seqs = _read_fasta_seqs(path) - if not seqs: - return {} - alnlen = len(seqs[0][1]) - nseq = len(seqs) - total_chars = 0 - total_gaps = 0 - total_gap_opens = 0 - for _, s in seqs: - in_gap = False - for ch in s: - if ch == "-": - total_gaps += 1 - if not in_gap: - total_gap_opens += 1 - in_gap = True - else: - total_chars += 1 - in_gap = False - gap_frac = total_gaps / (nseq * alnlen) if alnlen > 0 else 0 - mean_seqlen = total_chars / nseq if nseq > 0 else 0 - alnlen_ratio = alnlen / mean_seqlen if mean_seqlen > 0 else 0 - return { - "alnlen": alnlen, - "nseq": nseq, - "gap_frac": gap_frac, - "gap_opens_per_seq": total_gap_opens / nseq if nseq > 0 else 0, - "alnlen_ratio": alnlen_ratio, - "mean_seqlen": mean_seqlen, - } - - -def _score_case(case, output_path): - xml_path = case.reference.with_suffix(".xml") - if xml_path.exists(): - mask = parse_balibase_xml(xml_path) - return kalign.compare_detailed(str(case.reference), str(output_path), column_mask=mask) - return kalign.compare_detailed(str(case.reference), str(output_path)) - - -CONFIGS = [ - { - "label": "baseline", - "refine": "none", - "vsm_amax": 0.0, # explicitly disable VSM - }, - { - "label": "+vsm", - "refine": "none", - "vsm_amax": 2.0, - }, - { - "label": "+vsm+ref", - "refine": "confident", - "vsm_amax": 2.0, - }, - { - "label": "+vsm+iref", - "refine": "inline", - "vsm_amax": 2.0, - }, - { - "label": "ens3", - "ensemble": 3, - "refine": "none", - "vsm_amax": 0.0, # no VSM in individual runs - }, - { - "label": "ens3+vsm", - "ensemble": 3, - "refine": "none", - "vsm_amax": 2.0, - }, - { - "label": "ens3+vsm+ref", - "ensemble": 3, - "refine": "confident", - "vsm_amax": 2.0, - }, - { - "label": "ens3+vsm+ref+ra1", - "ensemble": 3, - "refine": "confident", - "vsm_amax": 2.0, - "realign": 1, - }, - { - "label": "ens3+vsm+ref+ra2", - "ensemble": 3, - "refine": "confident", - "vsm_amax": 2.0, - "realign": 2, - }, - { - "label": "ens3+vsm+iref", - "ensemble": 3, - "refine": "inline", - "vsm_amax": 2.0, - }, - { - "label": "ens3+vsm+iref+ra1", - "ensemble": 3, - "refine": "inline", - "vsm_amax": 2.0, - "realign": 1, - }, -] - - -def _run_one(case, config): - label = config["label"] - with tempfile.NamedTemporaryFile(suffix=".fa", delete=False) as tmp: - tmp_path = tmp.name - try: - t0 = time.perf_counter() - kwargs = dict(format="fasta", seq_type=case.seq_type) - - # Alignment mode - if config.get("ensemble"): - kwargs["ensemble"] = config["ensemble"] - kwargs["refine"] = config.get("refine", "none") - kwargs["vsm_amax"] = config.get("vsm_amax", -1.0) - if config.get("realign"): - kwargs["realign"] = config["realign"] - else: - # Standard kalign - kwargs["refine"] = config.get("refine", "none") - kwargs["vsm_amax"] = config.get("vsm_amax", -1.0) - - kalign.align_file_to_file(str(case.unaligned), tmp_path, **kwargs) - wall = time.perf_counter() - t0 - scores = _score_case(case, tmp_path) - stats = _alignment_stats(tmp_path) - return { - "family": case.family, "dataset": case.dataset, - "method": label, - "recall": scores["recall"], "precision": scores["precision"], - "f1": scores["f1"], "tc": scores["tc"], - "wall_time": wall, - **stats, - } - except Exception as e: - return { - "family": case.family, "dataset": case.dataset, - "method": label, - "recall": 0, "precision": 0, "f1": 0, "tc": 0, - "wall_time": 0, - "error": str(e), - } - finally: - Path(tmp_path).unlink(missing_ok=True) - - -def _print_summary(results, method_names): - print(f"\n{'Method':>20} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8}" - f" {'GapFrac':>8} {'AlnRatio':>9} {'Time':>7}") - print("-" * 90) - groups = defaultdict(list) - for r in results: - groups[r["method"]].append(r) - for method in method_names: - entries = groups.get(method, []) - valid = [r for r in entries if "error" not in r] - errs = len(entries) - len(valid) - if not valid: - print(f"{method:>20} (no results)") - continue - rec = statistics.mean(r["recall"] for r in valid) - prec = statistics.mean(r["precision"] for r in valid) - f1 = statistics.mean(r["f1"] for r in valid) - tc = statistics.mean(r["tc"] for r in valid) - gf = statistics.mean(r.get("gap_frac", 0) for r in valid) - ar = statistics.mean(r.get("alnlen_ratio", 0) for r in valid) - wt = sum(r.get("wall_time", 0) for r in valid) - suffix = f" ({errs} err)" if errs else "" - print(f"{method:>20} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f}" - f" {gf:>8.3f} {ar:>9.2f} {wt:>6.1f}s{suffix}") - - -def _print_per_category(results, method_names): - all_cats = sorted({r["dataset"].replace("balibase_", "") for r in results}) - for cat in all_cats: - cat_results = [r for r in results if cat in r["dataset"]] - cat_groups = defaultdict(list) - for r in cat_results: - cat_groups[r["method"]].append(r) - - n = len(cat_groups.get(method_names[0], [])) - print(f"\n=== {cat} ({n} cases) ===") - print(f"{'Method':>20} {'Recall':>8} {'Prec':>8} {'F1':>8} {'TC':>8}") - print("-" * 55) - for method in method_names: - entries = [r for r in cat_groups.get(method, []) if "error" not in r] - if not entries: - continue - rec = statistics.mean(r["recall"] for r in entries) - prec = statistics.mean(r["precision"] for r in entries) - f1 = statistics.mean(r["f1"] for r in entries) - tc = statistics.mean(r["tc"] for r in entries) - print(f"{method:>20} {rec:>8.3f} {prec:>8.3f} {f1:>8.3f} {tc:>8.3f}") - - -def main(): - parser = argparse.ArgumentParser(description="Comprehensive BAliBASE benchmark") - parser.add_argument("--max-cases", type=int, default=0) - parser.add_argument("--categories", nargs="*", default=None) - parser.add_argument("--configs", nargs="*", default=None, - help="Only run specific configs by label") - args = parser.parse_args() - - cases = get_cases("balibase", max_cases=args.max_cases if args.max_cases else None) - if args.categories: - cats = [c.upper() for c in args.categories] - cases = [c for c in cases if any(cat in c.dataset.upper() for cat in cats)] - - configs = CONFIGS - if args.configs: - configs = [c for c in CONFIGS if c["label"] in args.configs] - - print(f"{len(cases)} BAliBASE cases, {len(configs)} configs", flush=True) - - method_names = [c["label"] for c in configs] - n_tasks = len(configs) * len(cases) - print(f"{n_tasks} total tasks (sequential)", flush=True) - - t0 = time.perf_counter() - results = [] - done = 0 - - for cfg in configs: - print(f"\n Config: {cfg['label']}", flush=True) - for case in cases: - r = _run_one(case, cfg) - results.append(r) - done += 1 - if done % 20 == 0: - elapsed = time.perf_counter() - t0 - eta = elapsed / done * (n_tasks - done) - print(f" {done}/{n_tasks} ({elapsed:.0f}s, ETA {eta:.0f}s)", flush=True) - - elapsed = time.perf_counter() - t0 - print(f"\nAll done in {elapsed:.0f}s") - - _print_summary(results, method_names) - _print_per_category(results, method_names) - - # Save - out = Path("benchmarks/data/vsm_ensemble_experiment.json") - out.parent.mkdir(parents=True, exist_ok=True) - with open(out, "w") as f: - json.dump(results, f, indent=2) - print(f"\nSaved to {out}") - - -if __name__ == "__main__": - main() diff --git a/docs/PRD-benchmark-repo-update.md b/docs/PRD-benchmark-repo-update.md deleted file mode 100644 index d315ab6..0000000 --- a/docs/PRD-benchmark-repo-update.md +++ /dev/null @@ -1,304 +0,0 @@ -# PRD: Benchmark Repo Update for Kalign v3.5 - -## Context - -The kalign Python bindings have been simplified to a clean two-path -architecture. The old parameter-based API (`ensemble=`, `refine=`, -`vsm_amax=`, `consistency=`, etc.) has been removed from the public -`kalign.align*()` functions. These functions now only accept `mode` -as the configuration mechanism. - -Full technical details: `docs/parameter-cleanup-integration.md` in the -kalign repo. - -**Important**: The benchmark repo's `CLAUDE.md` says "Do not edit -external code". All changes here are ONLY to the benchmark repo. The -kalign library itself is already updated and installed. - ---- - -## Prerequisites - -Install the updated kalign from source: - -```bash -cd /Users/timo/code/kalign -uv pip install -e . -``` - -Verify: -```bash -uv run python -c "import kalign; print(kalign.__version__); print(kalign.align(['ATCG','ATCGG']))" -``` - ---- - -## Kalign API Quick Reference - -### Standard alignment (mode-based) - -```python -import kalign - -# Three modes: "fast", "default", "accurate" -result = kalign.align_from_file("input.fasta", mode="default") -names, sequences = result # AlignedSequences unpacks as 2-tuple - -# File-to-file -kalign.align_file_to_file("input.fasta", "output.fasta", mode="default") -``` - -Parameters accepted by all three `align*()` functions: -- `mode`: `"fast"` | `"default"` | `"accurate"` (default: `"default"`) -- `seq_type`: `"auto"` | `"dna"` | `"rna"` | `"protein"` (default: `"auto"`) -- `gap_open`, `gap_extend`, `terminal_gap_extend`: optional float overrides - (when any is set, mode is forced to `"fast"`) -- `n_threads`: int (default: 1) - -No other parameters exist. There is no `ensemble`, `refine`, `vsm_amax`, -`realign`, `consistency`, `seq_weights`, etc. in the public API. These -are handled internally by the mode presets. - -### Optimizer path (for NSGA-III evaluation) - -```python -from kalign._core import ensemble_custom_file_to_file - -ensemble_custom_file_to_file( - input_file, output_file, - run_gpo=[...], run_gpe=[...], run_tgpe=[...], run_noise=[...], - run_types=[...], # per-run KALIGN_MATRIX_* constants - format="fasta", - seq_type=1, # fallback matrix if run_types empty - seed=42, # base seed (run k gets seed+k) - min_support=0, # POAR consensus threshold - refine=0, # KALIGN_REFINE_* constant - vsm_amax=-1.0, # -1.0 = C default - realign=0, - seq_weights=-1.0, # -1.0 = C default - n_threads=1, - consistency_anchors=0, - consistency_weight=2.0, -) -``` - -This is the ONLY way to pass fine-grained per-run parameters. It is used -exclusively by the optimizer, not by benchmark runners. - -### Scoring - -```python -import kalign - -# SP score (0-100) -sp = kalign.compare("reference.fasta", "test.fasta") - -# Detailed: recall, precision, F1, TC -d = kalign.compare_detailed("reference.fasta", "test.fasta", max_gap_frac=-1.0) -# d = {"recall": ..., "precision": ..., "f1": ..., "tc": ..., ...} - -# With BAliBASE XML column mask -d = kalign.compare_detailed("ref.fasta", "test.fasta", column_mask=[1,0,1,...]) -``` - ---- - -## Step 1: Update `src/runners.py` — `run_kalign()` - -### 1a. Simplify `run_kalign()` - -Remove all deprecated parameter kwargs. The function should only accept -`mode` and `seq_type`: - -```python -def run_kalign( - input_fasta: Path, - mode: str = "default", - seq_type: str = "auto", -) -> AlignResult: - """Run kalign via the Python API using mode presets.""" - import kalign as _kalign - - start = time.perf_counter() - result = _kalign.align_from_file( - str(input_fasta), - seq_type=seq_type, - mode=mode, - ) - wall = time.perf_counter() - start - ... -``` - -### 1b. Update `METHODS` registry - -```python -METHODS = { - "kalign": { - "fn": run_kalign, - "mode": "default", - }, - "kalign_fast": { - "fn": run_kalign, - "mode": "fast", - }, - "kalign_accurate": { - "fn": run_kalign, - "mode": "accurate", - }, - # External tools unchanged - "mafft": {"fn": run_mafft}, - "muscle": {"fn": run_muscle}, - "clustalo": {"fn": run_clustalo}, -} -``` - -Remove `"kalign_precise"` — use `"kalign_accurate"` instead. (If you -need backward compatibility with existing result files, keep `"precise"` -as an alias that maps to `mode="accurate"`.) - -### 1c. Update `METHOD_COLORS` and `METHOD_ORDER` - -Replace `"kalign_precise"` with `"kalign_accurate"` everywhere. - -### 1d. Update `run_method()` dispatcher - -After simplifying `run_kalign()`, the dispatcher should pass only -`mode` and `seq_type`. - -**Smoke test**: -```bash -uv run python -c " -from src.runners import run_method -from pathlib import Path -r = run_method('kalign_fast', Path('data/downloads/balibase/bb3_release/RV11/BB11001.tfa'), Path('/tmp/test')) -print(f'wall_time={r.wall_time:.2f}s') -" -``` - ---- - -## Step 2: Update `config/config.yaml` - -Replace `kalign_precise` with `kalign_accurate`: - -```yaml -methods: - kalign: - mode: default - kalign_fast: - mode: fast - kalign_accurate: - mode: accurate - mafft: {} - muscle: {} - clustalo: {} -``` - -Find-and-replace `kalign_precise` → `kalign_accurate` in: -- `config/config.yaml` -- Any Snakemake rules that reference method names - ---- - -## Step 3: Update optimizer imports (in kalign repo) - -**Note**: This is in `/Users/timo/code/kalign/benchmarks/`, not the -benchmark repo. Listed here for completeness. - -### 3a. Fix `matrix_map_int` - -```python -from kalign._core import MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66 - -"matrix_map_int": [MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66], -"matrix_map_str": ["pfasum43", "pfasum60", "corblosum66"], -``` - -### 3b. Keep `consistency` and `consistency_weight` in search space - -These are real per-run parameters passed through -`ensemble_custom_file_to_file()` → `kalign_run_config` → C engine. - -### 3c. Export JSON format for optimized presets - -```json -{ - "protein": { - "fast": { - "n_runs": 1, - "min_support": 0, - "runs": [{ - "matrix": "pfasum60", - "gpo": 8.4087, "gpe": 0.5153, "tgpe": 0.4927, - "vsm_amax": 1.448, "seq_weights": 1.063, - "dist_scale": 0.0, "realign": 0, "refine": 0, - "adaptive_budget": 0, "tree_seed": 42, "tree_noise": 0.1623, - "consistency_anchors": 0, "consistency_weight": 2.0 - }] - } - } -} -``` - ---- - -## Step 4: Run benchmark to verify - -```bash -cd /Users/timo/work/Documents/Manuscripts/2026_kalign_35 - -# Quick test -podman run --rm -v $(pwd):/work -w /work kalign-35-bench \ - snakemake --cores 4 results/summaries/alignment_accuracy.json -``` - -Or on host: -```bash -uv run python workflow/scripts/run_balibase_quick.py -``` - -Expected results for protein (BAliBASE, 218 families): -- `kalign` (default mode): F1 ~ 0.72-0.73, TC ~ 0.47 -- `kalign_fast`: F1 ~ 0.70-0.72 -- `kalign_accurate`: F1 ~ 0.76-0.77 - ---- - -## Step 5: Update figure scripts (cosmetic) - -Replace "precise" → "accurate" in labels and legends: -- `figures/fig_balibase.py` -- `figures/fig_bralibase.py` -- `figures/fig_speed.py` -- `figures/fig_summary_heatmap.py` -- `figures/style.py` - ---- - -## Implementation Order - -1. **Step 1** (runners.py) — critical, unblocks everything -2. **Step 2** (config.yaml) — quick, do with step 1 -3. **Step 3** (optimizer) — independent, in kalign repo -4. **Step 4** (verify) — after steps 1-2 -5. **Step 5** (figures) — cosmetic, can wait - -Steps 1-2 are in the benchmark repo. -Step 3 is in the kalign repo. -Steps 4-5 are in the benchmark repo. - ---- - -## Risk: Container Rebuild - -The benchmark container has kalign baked in. After the update, rebuild: - -```bash -cd /Users/timo/work/Documents/Manuscripts/2026_kalign_35 -bash containers/build.sh -``` - -Or for host-side testing: -```bash -cd /Users/timo/code/kalign && uv pip install -e . -``` From 9c7a24d5bb2e79966409186af9ef4f5f28e7d038 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 07:33:22 +0800 Subject: [PATCH 18/29] Remove dead code Six high-confidence dead items identified during pre-release audit. No callers anywhere in the source tree; not compiled, not exposed via the public API or bindings. - lib/src/coretralign.{c,h}: pthread-based scheduler superseded by lib/src/threadpool/; uncompiled (commented out in lib/CMakeLists.txt). - lib/src/mod_tldevel.h: 10-line wrapper header with zero includes. - lib/src/bpm.c bitShiftRight256ymm(): AVX2 helper, never called. Carried a stale "FIXME: not sure if this is correct!!!" comment. - lib/src/bisectingKmeans.c split(): replaced by split2() (parallel variant); never called. - python-kalign/io.py: unused 'import os'. - python-kalign/utils.py: unused 'Tuple' import. Verified: ctest 15/15 pass; pytest tests/python/ 170 pass (1 pre-existing test_module_exports failure unchanged). ~547 lines removed in total. --- lib/CMakeLists.txt | 3 - lib/src/bisectingKmeans.c | 273 -------------------------------------- lib/src/bpm.c | 15 --- lib/src/coretralign.c | 244 ---------------------------------- lib/src/coretralign.h | 71 ---------- lib/src/mod_tldevel.h | 10 -- python-kalign/io.py | 1 - python-kalign/utils.py | 2 +- 8 files changed, 1 insertion(+), 618 deletions(-) delete mode 100644 lib/src/coretralign.c delete mode 100644 lib/src/coretralign.h delete mode 100644 lib/src/mod_tldevel.h diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 406140d..51825e8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -57,9 +57,6 @@ set(source_files src/anchor_consistency.c src/msa_consistency.c src/ensemble.c - - # src/coretralign.c - # src/test.h ) # Add threadpool source when enabled diff --git a/lib/src/bisectingKmeans.c b/lib/src/bisectingKmeans.c index 081c33b..6e60516 100644 --- a/lib/src/bisectingKmeans.c +++ b/lib/src/bisectingKmeans.c @@ -58,8 +58,6 @@ static int bisecting_kmeans(struct msa* msa, struct node** ret_n, pair_dist_fn leaf_dist); /* static int bisecting_kmeans_parallel(struct msa* msa, struct node** ret_n, float** dm,int* samples, int num_samples); */ -static int split(const float * const * dm, int *samples, int num_anchors, int num_samples, - int seed_pick, struct kmeans_result **ret); static int split2(const float * const * dm,const int* samples, const int num_anchors,const int num_samples,const int seed_pick,struct kmeans_result** ret); #ifdef USE_THREADPOOL @@ -585,277 +583,6 @@ int bisecting_kmeans(struct msa* msa, struct node** ret_n, const float * const * /* return FAIL; */ /* } */ -int split(const float * const * dm,int* samples, int num_anchors,int num_samples,int seed_pick,struct kmeans_result** ret) -{ - struct kmeans_result* res = NULL; - int* sl = NULL; - int* sr = NULL; - int num_l,num_r; - float* w = NULL; - float* wl = NULL; - float* wr = NULL; - float* cl = NULL; - float* cr = NULL; - float dl = 0.0F; - float dr = 0.0F; - float score; - int num_var; - int i; - int s; - int j; - int stop = 0; - - num_var = num_anchors / 8; - if( num_anchors%8){ - num_var++; - } - num_var = num_var << 3; - - - - -#ifdef HAVE_AVX2 - wr = _mm_malloc(sizeof(float) * num_var,32); - wl = _mm_malloc(sizeof(float) * num_var,32); - cr = _mm_malloc(sizeof(float) * num_var,32); - cl = _mm_malloc(sizeof(float) * num_var,32); - w = _mm_malloc(sizeof(float) * num_var,32); -#else - MMALLOC(wr,sizeof(float) * num_var); - MMALLOC(wl,sizeof(float) * num_var); - MMALLOC(cr,sizeof(float) * num_var); - MMALLOC(cl,sizeof(float) * num_var); - MMALLOC(w,sizeof(float) * num_var); -#endif - - if(*ret){ - res = *ret; - }else{ - RUNP(res = alloc_kmeans_result(num_samples)); - } - - res->score = FLT_MAX; - - sl = res->sl; - sr = res->sr; - - - for(i = 0; i < num_var;i++){ - w[i] = 0.0F; - wr[i] = 0.0F; - wl[i] = 0.0F; - cr[i] = 0.0F; - cl[i] = 0.0F; - } - for(i = 0; i < num_samples;i++){ - s = samples[i]; - for(j = 0; j < num_anchors;j++){ - w[j] += dm[s][j]; - } - } - - for(j = 0; j < num_anchors;j++){ - w[j] /= (float)num_samples; - } - //r = tl_random_int(rng , num_samples); - //r = sel[t_iter]; - - s = samples[seed_pick]; - /* LOG_MSG("Selected %d\n",s); */ - for(j = 0; j < num_anchors;j++){ - cl[j] = dm[s][j]; - } - - for(j = 0; j < num_anchors;j++){ - cr[j] = w[j] - (cl[j] - w[j]); - fprintf(stdout,"BEGIN: %e %e diff::: %f %f\n", cl[j],cr[j], cl[j]-cr[j],w[j]); - - } - -#ifdef HAVE_AVX2 - _mm_free(w); -#else - MFREE(w); -#endif - - /* check if cr == cl - we have identical sequences */ - s = 0; - for(j = 0; j < num_anchors;j++){ - int res = cmp_floats(cl[j],cr[j]); - /* if(fabsf(cl[j]-cr[j]) > 1.0E-6){ */ - /* s = 1; */ - /* break; */ - /* } */ - if(res != 0){ - s++; - } - } - - fprintf(stdout,"S: %d\n",s); - s = 0; - for(j = 0; j < num_anchors;j++){ - /* int res = cmp_floats(dr,dl); */ - if(fabsf(cl[j]-cr[j]) > 1.0E-6){ - s++; - //break; - } - /* if(res != 0){ */ - /* s++; */ - /* } */ - } - - fprintf(stdout,"S: %d\n",s); -#ifdef HAVE_AVX2 - edist_256(cl,cr, num_anchors, &dr); -#else - edist_serial(cl, cr, num_anchors, &dr); -#endif - - - - fprintf(stdout,"R/L Dist: %e %e %d\n",dr,1e-6, dr < 1e-6); - cmp_floats(cl[j],cr[j]); - if(!s){ - score = 0.0F; - num_l = 0; - num_r = 0; - /* The code below caused sequence sets of size 1 to be passed to clustering... */ - /* sl[num_l] = samples[0]; */ - /* num_l++; */ - - /* for(i =1 ; i nl = num_l; - res->nr = num_r; - res->score = score; - *ret = res; - return OK; -ERROR: - return FAIL; -} - int split2(const float * const * dm,const int* samples, const int num_anchors,const int num_samples,const int seed_pick,struct kmeans_result** ret) { struct kmeans_result* res = NULL; diff --git a/lib/src/bpm.c b/lib/src/bpm.c index 2ecaf34..9aa0d52 100644 --- a/lib/src/bpm.c +++ b/lib/src/bpm.c @@ -18,7 +18,6 @@ __m256i BROADCAST_MASK[16]; void bitShiftLeft256ymm (__m256i *data, int count); -__m256i bitShiftRight256ymm (__m256i *data, int count); /* taken from Alexander Yee: http://www.numberworld.org/y-cruncher/internals/addition.html#ks_add */ __m256i add256(uint32_t carry, __m256i A, __m256i B); @@ -335,20 +334,6 @@ void bitShiftLeft256ymm (__m256i *data, int count) //return carryOut; } -__m256i bitShiftRight256ymm (__m256i *data, int count) -{ - __m256i innerCarry, carryOut, rotate; - - - innerCarry = _mm256_slli_epi64(*data, 64 - count); - rotate = _mm256_permute4x64_epi64 (innerCarry, 0x39); - innerCarry = _mm256_blend_epi32 (_mm256_setzero_si256 (), rotate, 0x3F); - *data = _mm256_srli_epi64(*data, count); - *data = _mm256_or_si256(*data, innerCarry); - - carryOut = _mm256_xor_si256 (innerCarry, rotate); //FIXME: not sure if this is correct!!! - return carryOut; -} #endif diff --git a/lib/src/coretralign.c b/lib/src/coretralign.c deleted file mode 100644 index 5c89bc8..0000000 --- a/lib/src/coretralign.c +++ /dev/null @@ -1,244 +0,0 @@ -#include "tldevel.h" - -#define CORETRALIGN_IMPORT -#include "coretralign.h" - -int aln_scheduler_get_tid(aln_scheduler *s) -{ - int i; - int64_t tid = (int64_t) pthread_self(); - int ID = -1; - aln_scheduler_lock(s); - for(i = 0; i < s->thread_id_idx;i++){ - if(s->thread_id_map[i] == tid){ - ID = i; - break; - } - } - if(ID == -1){ - ID = s->thread_id_idx; - s->thread_id_map[s->thread_id_idx] = tid; - s->thread_id_idx++; - } - aln_scheduler_unlock(s); - return ID; -} - -int aln_scheduler_lock(aln_scheduler* s) -{ - ASSERT(s != NULL, "No thread controll"); - /* pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); */ - if(pthread_mutex_lock(&s->lock) != 0){ - ERROR_MSG("Can't get lock"); - } - return OK; -ERROR: - return FAIL; -} - -int aln_scheduler_trylock(aln_scheduler* s) -{ - return pthread_mutex_trylock(&s->lock); -} - -int aln_scheduler_unlock(aln_scheduler* s) -{ - ASSERT(s != NULL, "No thread controll"); - if(pthread_mutex_unlock(&s->lock) != 0){ - ERROR_MSG("Can't get lock"); - } - /* pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); */ - /* pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); */ - /* pthread_testcancel(); */ - return OK; -ERROR: - return FAIL; -} - - -int aln_scheduler_alloc(aln_scheduler **scheduler, int n_threads) -{ - aln_scheduler* n = NULL; - - MMALLOC(n, sizeof(aln_scheduler)); - n->task_alloc = NULL; - n->task_queue = NULL; - n->root = NULL; - n->threads = NULL; - n->thread_id_map = NULL; - n->thread_id_idx = 0; - pthread_mutex_init(&n->lock, NULL); - n->n_threads = n_threads; - - MMALLOC(n->threads, sizeof(pthread_t) * n->n_threads); - - MMALLOC(n->thread_id_map, sizeof(int64_t) * n->n_threads); - - - queue_alloc(&n->task_alloc, 1); - queue_alloc(&n->task_queue, 0); - - - - *scheduler = n; - return OK; -ERROR: - return FAIL; -} - -void aln_scheduler_free(aln_scheduler *n) -{ - if(n){ - if(n->task_alloc){ - queue_free(n->task_alloc); - } - if(n->task_queue){ - queue_free(n->task_queue); - } - if(n->threads){ - MFREE(n->threads); - } - if(n->thread_id_map){ - MFREE(n->thread_id_map); - } - MFREE(n); - } - -} - - - -static int aln_elem_alloc(aln_elem **node); -static int aln_elem_free(aln_elem *n); - -int queue_push(aln_elem_queue *q, aln_elem *n) -{ - if(q->head == NULL){ - q->head = n; - q->tail = q->head; - q->head->p = NULL; - }else{ - n->p = q->head; - q->head = n; - } - q->n++; - return OK; -} - -int queue_pop(aln_elem_queue *q, aln_elem **node) -{ - aln_elem * tmp = NULL; - if(q->head == NULL){ - if(q->is_allocator){ - /* RUN(eq_add_mem(q)); */ - }else{ - *node = NULL; - return FAIL; - } - } - - *node = q->head; - tmp = q->head; - q->head = tmp->p; - - if(q->head == NULL){ - q->tail = NULL; - } - q->n--; - - return OK; -} - -int queue_append(aln_elem_queue *q, aln_elem* n) -{ - - if(q->head == NULL){ - q->head = n; - q->tail = q->head; - - }else{ - q->tail->p = n; - q->tail = q->tail->p; - } - q->tail->p = NULL; - q->n++; - return OK; -} - - -int queue_add_mem(aln_elem_queue *q) -{ - ASSERT(q->alloc_block_size != 0,"This queue is not an allocator!"); - - for(int i = 0; i < q->alloc_block_size;i++){ - /* Alloc e_node */ - aln_elem* n = NULL; - aln_elem_alloc(&n); - /* Add to queue */ - queue_append(q, n); - } - return OK; -ERROR: - - return FAIL; -} - -int queue_alloc(aln_elem_queue **queue, uint8_t is_allocator) -{ - aln_elem_queue* l = NULL; - MMALLOC(l, sizeof(aln_elem_queue)); - l->head = NULL; - l->tail = 0; - l->n = 0; - l->alloc_block_size = 0; - - l->is_allocator = is_allocator; - if(is_allocator){ - l->alloc_block_size = 4096; - RUN(queue_add_mem(l)); - /* RUN(eq_add_mem(l)); */ - } - *queue = l; - return OK; -ERROR: - queue_free(l); - return FAIL; -} - -void queue_free(aln_elem_queue *l) -{ - if(l){ - aln_elem* n = NULL; - while(l->n){ - n = NULL; - queue_pop(l, &n); - aln_elem_free(n); - } - MFREE(l); - } -} - -int aln_elem_alloc(aln_elem **node) -{ - aln_elem* n = NULL; - - MMALLOC(n, sizeof(aln_elem)); - n->l = NULL; - n->r = NULL; - n->p = NULL; - n->wait = 0; - n->type = AE_TYPE_UNDEF; - n->aln_mem = NULL; - *node = n; - return OK; -ERROR: - aln_elem_free(n); - return FAIL; -} - -int aln_elem_free(aln_elem *n) -{ - if(n){ - MFREE(n); - } -} diff --git a/lib/src/coretralign.h b/lib/src/coretralign.h deleted file mode 100644 index a8671da..0000000 --- a/lib/src/coretralign.h +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef CORETRALIGN_H -#define CORETRALIGN_H - -#include -#include -#include "aln_mem.h" - -#ifdef CORETRALIGN_IMPORT -#define EXTERN -#else -#define EXTERN extern -#endif - -/* Idea: - construct hirschberg alignment via trees - -> only because this would make threading easier to control -*/ - -typedef enum { - AE_TYPE_UNDEF, - AE_TYPE_ALN_BACK, - AE_TYPE_ALN_FORWARD, - AE_TYPE_SPLIT - /* tree stuff here */ -}aln_elem_type; - -typedef struct aln_elem aln_elem; -typedef struct aln_elem { - aln_elem* l; /* left OR backward in dyn*/ - aln_elem* r; /* right OR backward in dyn*/ - aln_elem* p; /* parent */ - uint8_t wait; - aln_elem_type type; - struct aln_mem* aln_mem; -} aln_elem; - -typedef struct aln_elem_queue { - aln_elem* head; - aln_elem* tail; - int alloc_block_size; - int n; - uint8_t is_allocator; -} aln_elem_queue; - -typedef struct aln_scheduler { - aln_elem_queue* task_alloc; - aln_elem_queue* task_queue; - aln_elem* root; - pthread_t* threads; - int64_t* thread_id_map; - int thread_id_idx; - pthread_mutex_t lock; - int n_threads; - double** dm; - struct msa* msa; -} aln_scheduler; - -EXTERN int aln_scheduler_lock(aln_scheduler* s); -EXTERN int aln_scheduler_trylock(aln_scheduler* s); -EXTERN int aln_scheduler_unlock(aln_scheduler* s); - -EXTERN int aln_scheduler_alloc(aln_scheduler **scheduler, int n_threads); -EXTERN void aln_scheduler_free(aln_scheduler *n); - -EXTERN int queue_alloc(aln_elem_queue **queue, uint8_t is_allocator); -EXTERN void queue_free(aln_elem_queue *l); - -#undef CORETRALIGN_IMPORT -#undef EXTERN - -#endif diff --git a/lib/src/mod_tldevel.h b/lib/src/mod_tldevel.h deleted file mode 100644 index 66c17bd..0000000 --- a/lib/src/mod_tldevel.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef MOD_TLDEVEL_H -#define MOD_TLDEVEL_H - - -#include "tldevel.h" -#include "tlrng.h" -#include "tlmisc.h" -#include "esl_stopwatch.h" - -#endif diff --git a/python-kalign/io.py b/python-kalign/io.py index d929233..9409852 100644 --- a/python-kalign/io.py +++ b/python-kalign/io.py @@ -5,7 +5,6 @@ and writing alignments in various formats, with optional Biopython integration. """ -import os from pathlib import Path from typing import List, Optional, TextIO, Tuple, Union diff --git a/python-kalign/utils.py b/python-kalign/utils.py index c432d88..6eecd1b 100644 --- a/python-kalign/utils.py +++ b/python-kalign/utils.py @@ -6,7 +6,7 @@ """ from collections import Counter -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional import numpy as np From e7e55b338a994b282802437d4aff8254b3ebb990 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 07:44:13 +0800 Subject: [PATCH 19/29] Tidy design-doc references; commit parameter-cleanup PRD - PRD_sparse_consistency.md, docs/PRD-msa-consistency.md: replace references to `optimize_unified.py` with a note pointing at the manuscript repository's scripts/optimizers/ copy. The optimizer itself was moved out of kalign in commit dd01498. - docs/PRD-parameter-cleanup.md: add to the repo. Adds a brief status note acknowledging that a fourth mode (`recall`) was added during implementation; the rest of the architecture description is current. --- PRD_sparse_consistency.md | 8 +- docs/PRD-msa-consistency.md | 7 +- docs/PRD-parameter-cleanup.md | 542 ++++++++++++++++++++++++++++++++++ 3 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 docs/PRD-parameter-cleanup.md diff --git a/PRD_sparse_consistency.md b/PRD_sparse_consistency.md index 2563b8d..0a9823b 100644 --- a/PRD_sparse_consistency.md +++ b/PRD_sparse_consistency.md @@ -295,10 +295,10 @@ Verify that MDSA families that crashed with dense implementation now complete: -o /tmp/test.fa --consistency 8 --realign 2 ``` -Run the full MDSA DNA benchmark: -```bash -uv run python -m benchmarks.optimize_unified --dataset mdsa --pop-size 20 --n-gen 2 --n-workers 4 -``` +Run the full MDSA DNA benchmark: the optimizer harness that produced the +nucleotide preset numbers (`optimize_unified.py`) now lives in the +manuscript repository at `scripts/optimizers/` and is no longer shipped +with kalign. ### 5.3 Existing test suite diff --git a/docs/PRD-msa-consistency.md b/docs/PRD-msa-consistency.md index aaef8dd..122aaa9 100644 --- a/docs/PRD-msa-consistency.md +++ b/docs/PRD-msa-consistency.md @@ -73,7 +73,11 @@ run k, that's a vote. Bonus = weight × (votes / K). - `consistency_merge` (int, default 0) - `consistency_merge_weight` (float, default 2.0) -### Optimizer: `optimize_unified.py` +### Optimizer search space + +The NSGA-III optimizer that derived the preset values now lives in the +manuscript repository (`scripts/optimizers/`); the search space for this +feature was: - `consistency_merge`: Choice({0, 1}), only when n_runs > 1 - `consistency_merge_weight`: Real([0.5, 10.0]), only when consistency_merge=1 @@ -95,4 +99,3 @@ benefit. The optimizer finds the sweet spot. Default 2.0. - `lib/src/ensemble.c` — consistency merge path - `lib/CMakeLists.txt` — new source file - `python-kalign/_core.cpp` — expose params -- `benchmarks/optimize_unified.py` — optimizer variables diff --git a/docs/PRD-parameter-cleanup.md b/docs/PRD-parameter-cleanup.md new file mode 100644 index 0000000..782ddd5 --- /dev/null +++ b/docs/PRD-parameter-cleanup.md @@ -0,0 +1,542 @@ +# PRD: Kalign Parameter Architecture Cleanup + +> **Status note:** this document captures the original three-mode design +> (`fast` / `default` / `accurate`). A fourth mode, `recall`, was added +> during implementation. The architecture below is otherwise current; +> mentally substitute "four modes × three biotypes" for the preset grid. + +## Goal + +Replace the current parameter specification system — which conflates sequence +types with matrix choices, uses sentinel values resolved at multiple depths, and +exposes internals to users — with a clean two-layer design: + +1. **User layer**: one parameter (`mode`) that selects a fully optimised preset. +2. **Engine layer**: one function (`kalign_align_full`) that takes an array of + fully concrete run configurations. + +The optimizer populates the engine layer directly. Users never see it. + +--- + +## 1. User-Facing API + +### Python + +```python +# The only parameter most users ever need: +result = kalign.align(sequences, mode="default") +result = kalign.align(sequences, mode="fast") +result = kalign.align(sequences, mode="accurate") + +# Force biotype when auto-detection is wrong (rare): +result = kalign.align(sequences, mode="default", seq_type="protein") + +# Threading: +result = kalign.align(sequences, mode="default", n_threads=4) +``` + +### Parameters exposed to users + +| Parameter | Type | Values | Default | +|------------------------|---------------|-------------------------------------|-------------| +| `mode` | str | `"fast"`, `"default"`, `"accurate"` | `"default"` | +| `seq_type` | str | `"auto"`, `"dna"`, `"rna"`, `"protein"` | `"auto"` | +| `n_threads` | int | >= 1 | 1 | +| `gap_open` | float or None | penalty value | None | +| `gap_extend` | float or None | penalty value | None | +| `terminal_gap_extend` | float or None | penalty value | None | + +Mode handles everything for the common case. The gap penalty parameters +exist for backward compatibility and expert use. + +### Gap penalty override rule + +When the user provides **any** gap penalty (`gap_open`, `gap_extend`, or +`terminal_gap_extend`), the following happens: + +1. The `mode` parameter is **ignored** (regardless of what was passed). +2. The `fast` preset for the detected biotype is loaded (single run). +3. The user's gap penalties **replace** the corresponding preset values. +4. Unspecified penalties keep the `fast` preset defaults. + +This gives expert users direct control over gap scoring while keeping +everything else (matrix, VSM, seq_weights, etc.) at sensible optimised +values. + +```python +# Uses default mode (5-run ensemble): +kalign.align(sequences) + +# Uses fast preset, overrides gap open only: +kalign.align(sequences, gap_open=5.0) + +# Uses fast preset, overrides all three: +kalign.align(sequences, gap_open=5.0, gap_extend=0.5, + terminal_gap_extend=0.3) + +# mode is ignored when gap penalties are set: +kalign.align(sequences, mode="accurate", gap_open=5.0) # → fast + gpo=5.0 +``` + +### File-based variants + +```python +# Read from file, return AlignedSequences (names + sequences) +result = kalign.align_from_file("input.fasta", mode="default") + +# Read from file, write to file +kalign.align_file_to_file("input.fasta", "output.fasta", + mode="default", format="fasta") +``` + +`align_file_to_file` adds one extra parameter: + +| Parameter | Type | Values | Default | +|-----------|------|---------------------------------|-----------| +| `format` | str | `"fasta"`, `"msf"`, `"clu"` | `"fasta"` | + +### Output formats + +- `align()` returns `List[str]` (aligned sequences) or ecosystem objects + via `fmt="biopython"` / `fmt="skbio"`. +- `align_from_file()` returns `AlignedSequences` (names + sequences + + optional confidence). +- `align_file_to_file()` writes to disk, returns nothing. + +### CLI + +```bash +kalign -i input.fasta -o output.fasta # mode=default +kalign -i input.fasta -o output.fasta --mode fast +kalign -i input.fasta -o output.fasta --mode accurate +kalign -i input.fasta -o output.fasta --type protein # force biotype +kalign -i input.fasta -o output.fasta --gpo 5.0 # fast + gpo override +``` + +--- + +## 2. Mode Presets + +Each mode is a static lookup table of fully concrete run configurations. The +number of runs, the matrix per run, the gap penalties per run, the tree seed +per run — everything is baked in. No computation, no scaling, no indirection. + +Presets are **biotype-specific**: protein, DNA, and RNA each have their own +optimised configurations. + +### Mode semantics + +Three modes trade speed against accuracy. The user's only decision is where +on that tradeoff they want to be. + +| Mode | Intent | +|------------|-----------------------------------------------------| +| `fast` | Fastest possible alignment, acceptable quality | +| `default` | Good balance of speed and accuracy for routine use | +| `accurate` | Best achievable accuracy, speed secondary | + +### Preset grid + +There are **9 preset slots**: 3 modes × 3 biotypes. + +| | Protein | DNA | RNA | +|------------|---------|-----|-----| +| `fast` | slot | slot| slot| +| `default` | slot | slot| slot| +| `accurate` | slot | slot| slot| + +Each slot is filled by the optimizer independently. The number of runs, +the choice of matrices, the gap penalties, whether realignment is used — +all of this is determined per slot by multi-objective optimization, not +prescribed by the PRD. + +Constraints on the optimizer: +- `n_runs` must be 1, 3, or 5 (no other ensemble sizes). +- `fast` should be Pareto-optimal for speed; `accurate` for quality; + `default` for balanced. +- Within each biotype, `fast` must be strictly faster than `default`, + and `default` strictly faster than `accurate`. + +### Preset structure (per mode × biotype) + +Each preset specifies: + +**Ensemble-level parameters** (structural — not per-run): + +| Field | Description | +|----------------|-------------------------------------------------| +| `n_runs` | Number of alignment runs (1, 3, or 5) | +| `min_support` | POAR consensus column threshold (ensemble only) | + +These are inherently about the ensemble as a whole, not about any +individual alignment run. + +**Per-run parameters** (each run in the ensemble has its own values): + +| Field | Description | +|----------------|-------------------------------------------------| +| `matrix` | Substitution matrix for this run | +| `gpo` | Gap open penalty | +| `gpe` | Gap extend penalty | +| `tgpe` | Terminal gap extend penalty | +| `vsm_amax` | Variable scoring matrix amplitude (0 = off) | +| `seq_weights` | Profile rebalancing pseudo-count (0 = off) | +| `dist_scale` | Distance-dependent gap scaling (0 = off) | +| `realign` | Alignment-guided tree-rebuild iterations (0+) | +| `refine` | Post-alignment refinement strategy | +| `adaptive_budget` | Scale refinement trials by uncertainty (0=off)| +| `tree_seed` | RNG seed for guide tree construction | +| `tree_noise` | Guide tree perturbation sigma | + +The optimizer may choose identical values across runs for some +parameters (e.g. the same `vsm_amax` for all 5 runs) or vary them +(e.g. `realign=2` on run 1 but `realign=0` on run 3). That is the +optimizer's decision, not an architectural constraint. + +This is the **complete parameter space** exposed to the optimizer. + +--- + +## 3. Engine Layer (C API) + +### Substitution matrix constants + +```c +#define KALIGN_MATRIX_AUTO 0 /* auto-select for biotype */ +#define KALIGN_MATRIX_PFASUM43 1 /* 1/3 bit, divergent protein */ +#define KALIGN_MATRIX_PFASUM60 2 /* 1/3 bit, moderate protein */ +#define KALIGN_MATRIX_CORBLOSUM66 3 /* 1/3 bit, close protein */ +#define KALIGN_MATRIX_DNA 4 /* DNA match/mismatch (+5/-4) */ +#define KALIGN_MATRIX_DNA_INTERNAL 5 /* DNA internal (tgpe=8) */ +#define KALIGN_MATRIX_RNA 6 /* RNA RIBOSUM-like (~160-383) */ +``` + +Every value maps to exactly one scoring table. No duplicates. + +GONNET (gon250) remains in the source as dead code but is not assigned a +constant and is unreachable through any API. + +### Matrix default penalties + +Each matrix has intrinsic default gap penalties. These are used as starting +points when constructing configs, never resolved at alignment time. + +| Matrix | gpo | gpe | tgpe | Score range | +|---------------|--------|-------|--------|---------------| +| PFASUM43 | 7.0 | 1.25 | 1.0 | -6 to 13 | +| PFASUM60 | 7.0 | 1.25 | 1.0 | -6 to 14 | +| CorBLOSUM66 | 5.5 | 2.0 | 1.0 | -4 to 13 | +| DNA | 8.0 | 6.0 | 0.0 | -4 to 5 | +| DNA_INTERNAL | 8.0 | 6.0 | 8.0 | -4 to 5 | +| RNA | 217.0 | 39.4 | 292.6 | ~160 to 383 | + +PFASUM43, PFASUM60, and CorBLOSUM66 are all in 1/3-bit units. Their gap +penalties are directly comparable. A penalty value like `gpo=7.0` means the +same thing across all three matrices. + +### Refinement constants + +```c +#define KALIGN_REFINE_NONE 0 /* no post-alignment refinement */ +#define KALIGN_REFINE_ALL 1 /* refine all columns (two-pass) */ +#define KALIGN_REFINE_CONFIDENT 2 /* refine high-confidence columns */ +#define KALIGN_REFINE_INLINE 3 /* per-node refinement during tree */ +``` + +### Run config struct + +```c +struct kalign_run_config { + int matrix; /* KALIGN_MATRIX_* */ + float gpo; /* gap open penalty (concrete, no sentinel) */ + float gpe; /* gap extend penalty */ + float tgpe; /* terminal gap extend penalty */ + float vsm_amax; /* variable scoring matrix amplitude (0=off) */ + float seq_weights; /* profile rebalancing pseudo-count (0=off) */ + float dist_scale; /* distance-dependent gap scaling (0=off) */ + int refine; /* KALIGN_REFINE_* */ + int adaptive_budget; /* scale refinement by uncertainty (0=off) */ + int realign; /* tree-rebuild iterations (0=none) */ + uint64_t tree_seed; /* guide tree RNG seed */ + float tree_noise; /* guide tree perturbation sigma (0=none) */ +}; +``` + +No sentinel values. Every field is a concrete, usable value. A config +obtained from `kalign_run_config_defaults()` or `kalign_get_mode_preset()` +is directly usable without further resolution. + +### Ensemble config struct + +```c +struct kalign_ensemble_config { + int min_support; /* POAR consensus column threshold */ +}; +``` + +Simplified from current struct. `seed` removed (each run has its own +`tree_seed`). `save_poar` removed (debug feature, not part of core API). + +### Preset function + +```c +int kalign_get_mode_preset(const char *mode, + int biotype, + struct kalign_run_config *runs, + int *n_runs, + struct kalign_ensemble_config *ens); +``` + +- `mode`: `"fast"`, `"default"`, `"accurate"` (case-insensitive). +- `biotype`: `ALN_BIOTYPE_PROTEIN`, `ALN_BIOTYPE_DNA`, or `ALN_BIOTYPE_RNA`. + Determined by auto-detection before this call. +- `runs`: caller-allocated array (minimum 8 elements). +- `n_runs`: filled with the number of runs in the preset. +- `ens`: filled with ensemble config. +- Returns 0 on success, -1 on unknown mode. + +### Alignment entry point + +```c +int kalign_align_full(struct msa *msa, + const struct kalign_run_config *runs, + int n_runs, + const struct kalign_ensemble_config *ens, + int n_threads); +``` + +This is the single entry point for all alignment. It receives fully concrete +configs and executes them. No parameter resolution, no sentinel handling. + +If `n_runs == 1`: single alignment using `runs[0]`. +If `n_runs > 1`: ensemble — run each config, build POAR, consensus. + +When `runs[k].matrix == KALIGN_MATRIX_AUTO`: +- If `msa->biotype == DNA`: resolves to `KALIGN_MATRIX_DNA`, uses DNA + default penalties (overriding the config's gpo/gpe/tgpe). +- If `msa->biotype == RNA`: resolves to `KALIGN_MATRIX_RNA`, uses RNA + default penalties. +- If `msa->biotype == protein`: resolves to PFASUM43 or PFASUM60 using + the length-ratio heuristic (ratio < 1.5 → PFASUM43, else PFASUM60). + Keeps the config's gpo/gpe/tgpe. + +This is the **only** place where AUTO is resolved, and it happens **once** +before the alignment loop. + +--- + +## 4. Parameters Exposed to the Optimizer + +The optimizer sees the engine layer directly. It constructs +`kalign_run_config` arrays and calls `kalign_align_full`. + +### Optimizer search space + +**Ensemble-level parameters** (one value per preset slot): + +| Parameter | Type | Range | Description | +|----------------|--------|--------------------|--------------------------------| +| `n_runs` | int | {1, 3, 5} | Number of ensemble runs | +| `min_support` | int | [0, n_runs] | POAR consensus threshold | + +**Per-run parameters** (optimised independently for each of the n_runs): + +| Parameter | Type | Range | Description | +|----------------|--------|--------------------|--------------------------------| +| `matrix` | int | {1, 2, 3} | PFASUM43, PFASUM60, CorBLOSUM66| +| `gpo` | float | [1.0, 20.0] | Gap open penalty | +| `gpe` | float | [0.1, 5.0] | Gap extend penalty | +| `tgpe` | float | [0.1, 5.0] | Terminal gap extend penalty | +| `vsm_amax` | float | [0.0, 3.0] | VSM amplitude (0 = disabled) | +| `seq_weights` | float | [0.0, 3.0] | Pseudo-count (0 = disabled) | +| `dist_scale` | float | [0.0, 2.0] | Distance gap scaling (0 = off) | +| `realign` | int | {0, 1, 2, 3} | Tree-rebuild iterations | +| `refine` | int | {0, 1, 2, 3} | Refinement strategy | +| `adaptive_budget` | int | {0, 1} | Scale refinement by uncertainty| +| `tree_seed` | uint64 | fixed per run index | Deterministic seed | +| `tree_noise` | float | [0.0, 0.5] | Tree perturbation | + +The matrix values {1, 2, 3} correspond to the three protein matrices on +the same 1/3-bit scale, so a single set of gap penalty ranges works for +all of them. + +For DNA/RNA optimisation, the matrix field is fixed (DNA or RNA) and the +penalty ranges are adjusted to match those matrices' scales. + +The optimizer may choose to use the same value for a parameter across all +runs (e.g. `vsm_amax=2.0` for every run) or vary it per run. That is a +search strategy decision, not an architectural constraint. + +### What the optimizer produces + +A JSON file with this structure: + +```json +{ + "protein": { + "fast": { + "n_runs": 1, + "min_support": 0, + "runs": [ + { + "matrix": "pfasum60", + "gpo": 8.4087, + "gpe": 0.5153, + "tgpe": 0.4927, + "vsm_amax": 1.448, + "seq_weights": 1.063, + "dist_scale": 0.0, + "realign": 0, + "refine": 0, + "adaptive_budget": 0, + "tree_seed": 42, + "tree_noise": 0.1623 + } + ] + }, + "default": { + "n_runs": 5, + "min_support": 3, + "runs": [ { ... }, { ... }, { ... }, { ... }, { ... } ] + }, + "accurate": { + "n_runs": 5, + "min_support": 3, + "runs": [ { ... }, { ... }, { ... }, { ... }, { ... } ] + } + }, + "dna": { + "fast": { ... }, + "default": { ... }, + "accurate": { ... } + }, + "rna": { + "fast": { ... }, + "default": { ... }, + "accurate": { ... } + } +} +``` + +Each run object contains every per-run parameter. The JSON maps 1:1 to the +C preset tables. No interpretation, no transformation. The values in the +JSON are the values used in alignment. + +--- + +## 5. Removed Parameters + +The following are **removed from the user-facing API**. They remain +accessible only through the engine layer (`kalign_align_full`) for +benchmarking and optimiser use. + +| Removed parameter | Reason | +|------------------------|-----------------------------------------------| +| `ensemble` | Set by mode preset (1, 3, or 5) | +| `ensemble_seed` | Replaced by per-run `tree_seed` | +| `matrix` / `seq_type` | Set by mode preset per run | +| `vsm_amax` | Set by mode preset | +| `seq_weights` | Set by mode preset | +| `realign` | Set by mode preset | +| `refine` | Set by mode preset | +| `min_support` | Set by mode preset | +| `consistency` | Set by mode preset (currently always 0) | +| `consistency_weight` | Set by mode preset (dead when consistency=0) | +| `dist_scale` | Set by mode preset | +| `adaptive_budget` | Set by mode preset | +| `save_poar` | Debug feature, not user-facing | +| `load_poar` | Debug feature, not user-facing | + +--- + +## 6. Backward Compatibility + +### C header + +Old `KALIGN_TYPE_*` constants become `#define` aliases: + +```c +/* Deprecated — use KALIGN_MATRIX_* */ +#define KALIGN_TYPE_DNA KALIGN_MATRIX_DNA +#define KALIGN_TYPE_DNA_INTERNAL KALIGN_MATRIX_DNA_INTERNAL +#define KALIGN_TYPE_RNA KALIGN_MATRIX_RNA +#define KALIGN_TYPE_PROTEIN KALIGN_MATRIX_PFASUM43 +#define KALIGN_TYPE_PROTEIN_PFASUM43 KALIGN_MATRIX_PFASUM43 +#define KALIGN_TYPE_PROTEIN_PFASUM60 KALIGN_MATRIX_PFASUM60 +#define KALIGN_TYPE_PROTEIN_PFASUM_AUTO KALIGN_MATRIX_AUTO +#define KALIGN_TYPE_UNDEFINED KALIGN_MATRIX_AUTO +#define KALIGN_TYPE_PROTEIN_CORBLOSUM66 KALIGN_MATRIX_CORBLOSUM66 +``` + +`KALIGN_TYPE_PROTEIN_DIVERGENT` gets no alias — it mapped to GONNET +which is dead code. Any code using it gets a compile error, directing +the author to pick a specific matrix. + +Old functions (`kalign_run`, `kalign_run_seeded`, `kalign_ensemble`, +etc.) remain as thin wrappers that build a config and call +`kalign_align_full`. They accept -1.0 sentinels for backward compat, +resolving them into the config immediately. New code should not use them. + +### Python + +`align()`, `align_from_file()`, and `align_file_to_file()` continue to +accept all current keyword arguments during a deprecation period. When +old-style parameters are used, a `DeprecationWarning` is raised. The +old parameters are resolved into a run config and passed to the engine. + +After the deprecation period, the signatures shrink to: + +```python +def align(sequences, *, mode="default", seq_type="auto", + gap_open=None, gap_extend=None, terminal_gap_extend=None, + n_threads=None, fmt="plain", ids=None) + +def align_from_file(input_file, *, mode="default", seq_type="auto", + gap_open=None, gap_extend=None, + terminal_gap_extend=None, n_threads=None) + +def align_file_to_file(input_file, output_file, *, mode="default", + seq_type="auto", gap_open=None, gap_extend=None, + terminal_gap_extend=None, format="fasta", + n_threads=None) +``` + +Gap penalty parameters are retained permanently (not deprecated). When +any is set, the gap penalty override rule applies: mode is ignored, the +`fast` preset is loaded, and user penalties replace preset defaults. + +--- + +## 7. Implementation Order + +1. **Add `KALIGN_MATRIX_*` constants** to `kalign.h`. Add backward-compat + aliases for `KALIGN_TYPE_*`. Add `biotype` parameter to + `kalign_get_mode_preset`. + +2. **Rename `type` → `matrix`** in `kalign_run_config`. Update all code + that reads the field. + +3. **Remove sentinels from the new path.** `kalign_run_config_defaults()` + returns concrete PFASUM43 protein values. `aln_param_init` uses values + directly (no `if(gpo >= 0)` guard). Old functions keep sentinel + resolution internally. + +4. **Fix v2 presets.** Change all `KALIGN_TYPE_PROTEIN_DIVERGENT` → + `KALIGN_MATRIX_PFASUM43` in `kalign_get_mode_preset()`. These runs + were always PFASUM43; the "gonnet" label was a bug. + +5. **Add DNA/RNA preset stubs** in `kalign_get_mode_preset()`. Initially + use matrix defaults; optimise later. + +6. **Simplify Python API.** New signatures with `mode` + `seq_type` + + `n_threads`. Old parameters accepted with deprecation warnings. + Remove `_MODE_PRESETS` dict and `-1.0` sentinel logic from Python. + +7. **Update optimizer** to use `KALIGN_MATRIX_*` constants and produce + the JSON format described in section 4. + +8. **Benchmark** all three modes × all three biotypes. Verify protein + results match optimizer predictions now that the matrix bug is fixed. + +9. **Remove deprecated parameters** after one release cycle. From fb0d5c43a83ac71b3a814ef3fc6aa1dc39f96675 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 07:57:58 +0800 Subject: [PATCH 20/29] Clean up build warnings and stale libomp installs in CI Build now compiles warning-free. Warning fixes: - lib/src/euclidean_dist.c: drop unused local 'd2' in the UTEST_EDIST main() block. - lib/src/msa_io.c: drop unused local 'line_len' (set on every iteration of the line-scan loop but never read). - lib/src/msa_io.c: change 'size_t nread' to 'ssize_t nread' so the getline() return-value check against -1 is signed-correct. - tests/kalign_lib_testCXX.cpp: cast string-literal initialisers to char* so the array-of-char* initialisation no longer warns under -Wwritable-strings. CI cleanup: - .github/workflows/{cmake,python,benchmark}.yml: drop apt 'libomp-dev' and brew 'libomp' installs. Threadpool is the default parallelism backend (USE_OPENMP=OFF, USE_THREADPOOL=ON) so libomp is no longer needed for these jobs. The explanatory comment in wheels.yml remains. --- .github/workflows/benchmark.yml | 2 +- .github/workflows/cmake.yml | 6 +++--- .github/workflows/python.yml | 8 ++++---- lib/src/euclidean_dist.c | 1 - lib/src/msa_io.c | 14 +------------- tests/kalign_lib_testCXX.cpp | 8 ++++---- 6 files changed, 13 insertions(+), 26 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index fe6feb6..5dbecd5 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -37,7 +37,7 @@ jobs: - name: Install system dependencies run: | sudo apt-get update - sudo apt-get install -y libomp-dev cmake + sudo apt-get install -y cmake - name: Build C binary run: | diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index d746956..c9f819a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -35,12 +35,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y libomp-dev cmake + sudo apt-get install -y cmake - name: Install dependencies (macOS) if: runner.os == 'macOS' run: | - brew install --formula libomp cmake + brew install --formula cmake - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} @@ -83,7 +83,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libomp-dev cmake + sudo apt-get install -y cmake - name: Configure CMake with ASAN run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=ASAN diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 1be1b89..be919ff 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -68,12 +68,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y libomp-dev cmake + sudo apt-get install -y cmake - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | - brew install libomp cmake + brew install cmake - name: Install Python dependencies run: | @@ -116,12 +116,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y libomp-dev cmake + sudo apt-get install -y cmake - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | - brew install libomp cmake + brew install cmake - name: Install build dependencies run: | diff --git a/lib/src/euclidean_dist.c b/lib/src/euclidean_dist.c index d69af7f..dc9b959 100644 --- a/lib/src/euclidean_dist.c +++ b/lib/src/euclidean_dist.c @@ -33,7 +33,6 @@ int main(void) float** mat = NULL; double r; float d1; - float d2; int i,j,c; int max_iter = 10; int num_element = 128; diff --git a/lib/src/msa_io.c b/lib/src/msa_io.c index 8ffd908..fba3438 100644 --- a/lib/src/msa_io.c +++ b/lib/src/msa_io.c @@ -319,7 +319,6 @@ int detect_alignment_format(struct in_buffer*b,int* type) //char line[BUFFER_LEN]; int hints[3]; - int line_len; int line_number; int set; int i; @@ -335,18 +334,7 @@ int detect_alignment_format(struct in_buffer*b,int* type) } for(i = 0; i < MACRO_MIN(b->n_lines, 100);i++){ line = b->l[i]->line; - line_len = b->l[i]->len; - - //} - //RUNP(f_ptr = fopen(infile, "r")); - - /* scan through first line header */ - //while ((nread = getline(&line, &b_len, f_ptr)) != -1){ - //while(fgets(line, BUFFER_LEN, f_ptr)){ - //line_len = nread;//strnlen(line, BUFFER_LEN); - //line[line_len-1] = 0; - line_len--; if(line[0] == '>'){ hints[0]++; /* fasta */ } @@ -416,7 +404,7 @@ int read_file_stdin(struct in_buffer** buffer,char* infile) char* line = NULL; char* tmp = NULL; size_t b_len = 0; - size_t nread; + ssize_t nread; int i; //char line[BUFFER_LEN]; int line_len; diff --git a/tests/kalign_lib_testCXX.cpp b/tests/kalign_lib_testCXX.cpp index c79367d..18d8950 100644 --- a/tests/kalign_lib_testCXX.cpp +++ b/tests/kalign_lib_testCXX.cpp @@ -7,10 +7,10 @@ int main() { // Initialize array char * inseq[95] = { - "GKGDPKKPRGKMSSYAFFVQTSREEHKKKHPDASVNFSEFSKKCSERWKTMSAKEKGKFEDMAKADKARYEREMKTYIPPKGE", - "MQDRVKRPMNAFIVWSRDQRRKMALENPRMRNSEISKQLGYQWKMLTEAEKWPFFQEAQKLQAMHREKYPNYKYRPRRKAKMLPK", - "MKKLKKHPDFPKKPLTPYFRFFMEKRAKYAKLHPEMSNLDLTKILSKKYKELPEKKKMKYIQDFQREKQEFERNLARFREDHPDLIQNAKK", - "MHIKKPLNAFMLYMKEMRANVVAESTLKESAAINQILGRRWHALSREEQAKYYELARKERQLHMQLYPGWSARDNYGKKKKRKREK", + (char*)"GKGDPKKPRGKMSSYAFFVQTSREEHKKKHPDASVNFSEFSKKCSERWKTMSAKEKGKFEDMAKADKARYEREMKTYIPPKGE", + (char*)"MQDRVKRPMNAFIVWSRDQRRKMALENPRMRNSEISKQLGYQWKMLTEAEKWPFFQEAQKLQAMHREKYPNYKYRPRRKAKMLPK", + (char*)"MKKLKKHPDFPKKPLTPYFRFFMEKRAKYAKLHPEMSNLDLTKILSKKYKELPEKKKMKYIQDFQREKQEFERNLARFREDHPDLIQNAKK", + (char*)"MHIKKPLNAFMLYMKEMRANVVAESTLKESAAINQILGRRWHALSREEQAKYYELARKERQLHMQLYPGWSARDNYGKKKKRKREK", }; int numseq = 4; From eee0d938af6d64afc1ed90d5347ebe224935f377 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 08:01:45 +0800 Subject: [PATCH 21/29] Drop developer-only design docs from the source tree The four PRDs and the parameter-cleanup integration guide were internal planning documents describing work that's now complete. The current state of the API is documented in the public C header, the READMEs, the CLI --help, the Python docstrings, and the ChangeLog. The design rationale, where it's still relevant, lives in git history. Removed: - PRD_sparse_consistency.md - docs/PRD-msa-consistency.md - docs/PRD-confidence-masking-and-add-sequences.md - docs/PRD-parameter-cleanup.md - docs/parameter-cleanup-integration.md The docs/ directory is now empty and dropped from the tree; verified no README, code, or CI workflow references any of these files. --- PRD_sparse_consistency.md | 358 ------------ ...RD-confidence-masking-and-add-sequences.md | 313 ---------- docs/PRD-msa-consistency.md | 101 ---- docs/PRD-parameter-cleanup.md | 542 ------------------ docs/parameter-cleanup-integration.md | 337 ----------- 5 files changed, 1651 deletions(-) delete mode 100644 PRD_sparse_consistency.md delete mode 100644 docs/PRD-confidence-masking-and-add-sequences.md delete mode 100644 docs/PRD-msa-consistency.md delete mode 100644 docs/PRD-parameter-cleanup.md delete mode 100644 docs/parameter-cleanup-integration.md diff --git a/PRD_sparse_consistency.md b/PRD_sparse_consistency.md deleted file mode 100644 index 0a9823b..0000000 --- a/PRD_sparse_consistency.md +++ /dev/null @@ -1,358 +0,0 @@ -# PRD: Sparse Anchor Consistency Bonus Matrix - -## 1. Problem Statement - -The anchor consistency bonus matrix is currently stored as a dense `float[len_a * len_b]` array. This causes two critical failures for large DNA/RNA families: - -**Integer overflow in DP indexing.** The DP inner loops compute `i * m->consistency_stride + j` using `int` arithmetic. When `len_a` and `len_b` both exceed ~46,340, the product overflows 32-bit `int` (46,341 × 46,341 = 2,147,488,281 > INT_MAX). The resulting negative index causes SIGBUS/segfault. - -**Excessive memory.** A 50,000 × 50,000 dense matrix requires `50000 * 50000 * 4 = 10 GB` for a single merge step. With OpenMP parallelism, multiple merges may be in flight simultaneously. - -**The matrix is extremely sparse.** Each anchor `k` contributes at most one entry per row `i`. With K anchors (typically 8), each row has at most K non-zero entries. For a 50k × 50k matrix: 400,000 non-zero entries out of 2.5 billion cells (0.016% density). - -### Affected cases - -MDSA DNA benchmark families with large profile lengths during progressive alignment: - -| Family | Seqs | Max Length | N×MaxLen | -|--------|------|-----------|----------| -| RV60_sushi_ref6 | 384 | 5,178 | 1,988,352 | -| RV60_ank_ref6 | 210 | 9,297 | 1,952,370 | -| RV40_BB40047 | 54 | 13,644 | 736,776 | -| RV40_BB40023 | 22 | 23,769 | 522,918 | -| AAA (SMART) | 427 | 1,251 | 534,177 | - -## 2. Proposed Solution - -Replace the dense `float*` bonus matrix with a per-row sparse structure: - -```c -struct sparse_bonus { - int* cols; /* cols[i * K + k] = column index, or -1 if unused */ - float* vals; /* vals[i * K + k] = bonus value */ - int n_rows; /* = len_a (number of DP rows) */ - int K; /* max entries per row (= n_anchors) */ -}; -``` - -**Memory comparison:** - -| Scenario | Dense | Sparse (K=8) | Ratio | -|----------|-------|--------------|-------| -| Protein 500×500 | 1 MB | 32 KB | 32× | -| Protein 2000×2000 | 16 MB | 128 KB | 125× | -| DNA 50000×50000 | 10 GB | 3.2 MB | 3125× | - -**DP lookup** via inline function scanning K=8 slots per cell: - -```c -static inline float sparse_bonus_lookup(const struct sparse_bonus* sb, int i, int j) -{ - float bonus = 0.0f; - const int base = i * sb->K; - for(int k = 0; k < sb->K; k++){ - if(sb->cols[base + k] < 0) break; /* early exit: unused slots at end */ - if(sb->cols[base + k] == j) - bonus += sb->vals[base + k]; - } - return bonus; -} -``` - -## 3. Detailed Design - -### 3.1 New struct and functions in `anchor_consistency.h` - -Add after `struct consistency_table`: - -```c -struct sparse_bonus { - int* cols; - float* vals; - int n_rows; - int K; -}; - -static inline float sparse_bonus_lookup(const struct sparse_bonus* sb, int i, int j) -{ - float bonus = 0.0f; - const int base = i * sb->K; - for(int k = 0; k < sb->K; k++){ - if(sb->cols[base + k] < 0) break; - if(sb->cols[base + k] == j) - bonus += sb->vals[base + k]; - } - return bonus; -} - -EXTERN void sparse_bonus_free(struct sparse_bonus* sb); -``` - -Update function signatures: - -```c -EXTERN int anchor_consistency_get_bonus(struct consistency_table* ct, - int seq_a, int len_a, - int seq_b, int len_b, - struct sparse_bonus** bonus_out); - -EXTERN int anchor_consistency_get_bonus_profile(struct consistency_table* ct, - struct msa* msa, - int node_a, int len_a, - int node_b, int len_b, - struct sparse_bonus** bonus_out); -``` - -### 3.2 Changes to `aln_struct.h` - -Add forward declaration: -```c -struct sparse_bonus; -``` - -**Remove:** -```c -float* consistency; -int consistency_stride; -``` - -**Replace with:** -```c -struct sparse_bonus* consistency; -``` - -### 3.3 Changes to `aln_mem.c` - -In `alloc_aln_mem`, change: -```c -m->consistency = NULL; -m->consistency_stride = 0; -``` -To: -```c -m->consistency = NULL; -``` - -### 3.4 Rewrite bonus construction in `anchor_consistency.c` - -#### `sparse_bonus_free`: -```c -void sparse_bonus_free(struct sparse_bonus* sb) -{ - if(sb){ - if(sb->cols) MFREE(sb->cols); - if(sb->vals) MFREE(sb->vals); - MFREE(sb); - } -} -``` - -#### `anchor_consistency_get_bonus_profile` (and `_get_bonus`): - -Replace the dense allocation: -```c -MMALLOC(bonus, sizeof(float) * len_a * len_b); -memset(bonus, 0, sizeof(float) * len_a * len_b); -``` - -With sparse allocation: -```c -struct sparse_bonus* sb = NULL; -MMALLOC(sb, sizeof(struct sparse_bonus)); -sb->cols = NULL; -sb->vals = NULL; -sb->n_rows = len_a; -sb->K = K; - -MMALLOC(sb->cols, sizeof(int) * len_a * K); -MMALLOC(sb->vals, sizeof(float) * len_a * K); -for(i = 0; i < len_a * K; i++){ - sb->cols[i] = -1; - sb->vals[i] = 0.0f; -} -``` - -Replace the dense accumulation (current line 532): -```c -bonus[i * len_b + bj] += per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; -``` - -With sparse slot insertion: -```c -{ - float val = per_anchor_weight * conf_a[i] * inv_conf_b[ak_pos]; - int base = i * K; - int slot = -1; - for(int s = 0; s < K; s++){ - if(sb->cols[base + s] == bj){ slot = s; break; } /* existing entry */ - if(sb->cols[base + s] < 0){ slot = s; break; } /* empty slot */ - } - if(slot >= 0){ - sb->vals[base + slot] += val; - sb->cols[base + slot] = bj; - } -} -``` - -Note: the slot search handles both cases — accumulating into an existing entry (two anchors map the same row to the same column) and claiming a new slot. The `cols = bj` write is idempotent for existing entries. - -Return `*bonus_out = sb` instead of `*bonus_out = bonus`. - -### 3.5 Changes to DP consumer files (12 access sites) - -All 12 sites follow the same transformation. Current: -```c -if(m->consistency){ - pa += m->consistency[i * m->consistency_stride + j]; -} -``` - -Becomes: -```c -if(m->consistency){ - pa += sparse_bonus_lookup(m->consistency, ROW, COL); -} -``` - -Where ROW and COL are the same expressions currently used for `i` and `j`. - -**`aln_seqseq.c`** — 4 sites: -- Forward inner loop (line 83): ROW=`i`, COL=`j` -- Forward end-of-row (line 105): ROW=`i`, COL=`j` -- Backward inner loop (line 199): ROW=`starta + i`, COL=`j` -- Backward end-of-row (line 223): ROW=`starta + i`, COL=`j` - -**`aln_seqprofile.c`** — 4 sites: -- Forward inner loop (line 82): ROW=`i`, COL=`j` -- Forward end-of-row (line 107): ROW=`i`, COL=`j` -- Backward inner loop (line 193): ROW=`m->starta_2 + i`, COL=`j` -- Backward end-of-row (line 216): ROW=`m->starta_2 + i`, COL=`j` - -**`aln_profileprofile.c`** — 4 sites: -- Forward inner loop (line 108): ROW=`i`, COL=`j` -- Forward end-of-row (line 138): ROW=`i`, COL=`j` -- Backward inner loop (line 251): ROW=`m->starta_2 + i`, COL=`j` -- Backward end-of-row (line 280): ROW=`m->starta_2 + i`, COL=`j` - -### 3.6 Changes to `aln_run.c` - -**`do_align`** (2 locations): - -Setup (lines 259-261, 290-294): Remove `m->consistency_stride = dp_cols;` — stride is embedded in the sparse struct. - -Teardown (lines 401-405): Replace `MFREE(m->consistency)` with `sparse_bonus_free(m->consistency)`. - -**`do_align_inline_refine`** (2 locations): - -Setup (lines 566-567, 595-598): Same — remove stride assignment. - -Teardown (lines 736-740): Same — use `sparse_bonus_free`. - -## 4. Implementation Ordering - -Steps 1-2 are additive (nothing breaks). Steps 3-7 must be atomic (single commit). - -1. Add `struct sparse_bonus`, `sparse_bonus_lookup`, `sparse_bonus_free` to header/source -2. (Can be tested in isolation with a unit test) -3. Change `aln_struct.h` — replace `float* consistency` + `int consistency_stride` with `struct sparse_bonus*` -4. Update `aln_mem.c` — remove `consistency_stride` init -5. Update 12 DP access sites to use `sparse_bonus_lookup` -6. Rewrite `anchor_consistency_get_bonus` and `_get_bonus_profile` to produce sparse output -7. Update `aln_run.c` setup/teardown - -## 5. Testing Strategy - -### 5.1 Bit-exact regression (CRITICAL) - -For all BAliBASE protein families where the current implementation works correctly: -- Run alignment with current (dense) implementation, save output -- Run alignment with sparse implementation, compare output -- **Must be byte-identical** — same bonus values → same DP scores → same alignment paths - -```bash -# Save reference outputs with current code -for f in tests/data/*.tfa; do - ./build/src/kalign -i "$f" -o "/tmp/dense_$(basename $f)" --consistency 8 -done - -# After sparse implementation, compare -for f in tests/data/*.tfa; do - ./build/src/kalign -i "$f" -o "/tmp/sparse_$(basename $f)" --consistency 8 - diff "/tmp/dense_$(basename $f)" "/tmp/sparse_$(basename $f)" -done -``` - -Also run the full BAliBASE benchmark and compare F1/TC scores: -```bash -uv run python -m benchmarks.runner --dataset balibase -``` - -### 5.2 Large DNA families (new capability) - -Verify that MDSA families that crashed with dense implementation now complete: -```bash -./build-asan/src/kalign -i benchmarks/data/downloads/mdsa/unaligned/smart/AAA.afa \ - -o /tmp/test.fa --consistency 8 --realign 2 -``` - -Run the full MDSA DNA benchmark: the optimizer harness that produced the -nucleotide preset numbers (`optimize_unified.py`) now lives in the -manuscript repository at `scripts/optimizers/` and is no longer shipped -with kalign. - -### 5.3 Existing test suite - -All existing tests must pass unchanged: -```bash -cd build && make test # C tests -uv run pytest tests/python/ -v # Python tests -``` - -### 5.4 Performance comparison - -Time the BAliBASE benchmark with both implementations: -```bash -# Before (dense) -time uv run python -c " -from benchmarks.scoring import run_case -from benchmarks.datasets import balibase_cases -for c in balibase_cases(): - run_case(c, method='python_api', refine='none') -" - -# After (sparse) — same command -``` - -Expectation: equal or faster (better cache behavior). Any slowdown > 5% warrants investigation. - -### 5.5 Unit test for sparse_bonus - -Add a C test that: -1. Creates a `sparse_bonus` with known values -2. Verifies `sparse_bonus_lookup` returns correct values for filled slots -3. Returns 0.0f for empty slots and out-of-band columns -4. Correctly accumulates when two anchors map to the same (row, col) -5. Handles `sb == NULL` (returns 0.0f) - -## 6. Risk Analysis - -### 6.1 Incorrect accumulation -**Risk**: Two anchors mapping the same row to the same column must accumulate (add), not overwrite. -**Mitigation**: Slot-finding logic checks `cols[slot] == bj` before writing. The `+=` on vals handles accumulation. Unit test covers this case. - -### 6.2 Hirschberg sub-problem offsets -**Risk**: The divide-and-conquer DP uses `starta`, `starta_2` offsets for row indices. -**Mitigation**: The sparse lookup receives the same (row, col) coordinates as the dense indexing. No change in semantics. Bit-exact regression testing confirms correctness. - -### 6.3 Thread safety -**Risk**: Each merge step's `aln_mem` gets its own `sparse_bonus*`. The struct is read-only during DP. -**Mitigation**: Same thread-safety model as current dense implementation. No concurrent writes. - -### 6.4 Rollback strategy -The change is purely internal — no API, file format, or CLI changes. If any regression is found, revert the single commit to restore the dense implementation. - -## 7. Future Considerations - -- **Row-pointer hoisting**: Hoist `cols + i*K` and `vals + i*K` outside the j-loop for better codegen -- **SIMD lookup**: For K=8, a single AVX2 `_mm256_cmpeq_epi32` could find the matching slot in one instruction -- **Adaptive K**: If future anchor schemes use K > 16, switch to sorted columns with binary search diff --git a/docs/PRD-confidence-masking-and-add-sequences.md b/docs/PRD-confidence-masking-and-add-sequences.md deleted file mode 100644 index 303fdac..0000000 --- a/docs/PRD-confidence-masking-and-add-sequences.md +++ /dev/null @@ -1,313 +0,0 @@ -# PRD: Confidence masking + Add sequences to existing alignment - -## Feature 1: Confidence-filtered output - -### What - -Expose the per-column ensemble confidence scores to the user. Allow masking (lowercasing or gap-replacing) columns below a threshold. Only meaningful for ensemble modes (recall, accurate) which compute POAR confidence. - -### Why - -No other fast aligner provides per-column reliability. GUIDANCE2 does but is extremely slow (runs external aligners repeatedly). Kalign's ensemble already does the multiple runs — confidence is free. This turns kalign into a one-stop alignment + quality filter tool, directly useful for phylogenetics, positive selection, and structure prediction pipelines. - -### Paper angle - -**New figure or table:** Score BAliBASE alignments on ONLY the columns kalign considers confident (confidence >= threshold). Show that SP/TC on confident columns is dramatically higher than on all columns — proving the confidence scores are meaningful. Compare at several thresholds (0.3, 0.5, 0.7, 0.9). - -This directly addresses the TC weakness: "kalign's overall TC is lower than competitors, but if you trust its confidence scores and filter, the remaining columns have TC comparable to or better than MAFFT/MUSCLE." - -**Second comparison:** Run phylogenetic tree inference (IQ-TREE) on: -1. Full kalign accurate alignment -2. Kalign accurate filtered at confidence >= 0.5 -3. MAFFT alignment -4. MUSCLE alignment - -Compare Robinson-Foulds distance to the true tree (from INDELible simulations, which the manuscript already has). If filtered kalign gives better trees, that's a compelling result. - -### C CLI interface - -``` -kalign -i seqs.fa -o aligned.fa --mode accurate --confidence-threshold 0.7 - -Options: - --confidence-threshold FLOAT Mask columns with confidence below this value. - 0.0 = no masking (default). Requires ensemble mode. - --confidence-style STRING "lowercase" (default) or "remove". - lowercase: uncertain residues become lowercase - remove: uncertain columns replaced with gaps - --confidence-output FILE Write per-column confidence values to a separate file - (one float per line, one line per column) -``` - -The `--confidence-output` flag is useful for downstream tools that want the raw scores (e.g., custom trimming scripts). - -### Python API - -```python -# align() and align_from_file() already return confidence when using ensemble modes -result = kalign.align(sequences, mode="accurate") -# result.confidence is a list of per-column floats [0.0, 1.0] - -# New: mask_alignment convenience function -masked = kalign.mask_alignment(result, threshold=0.7, style="lowercase") -# masked.sequences has lowercase residues in low-confidence columns - -# New: filter_alignment removes low-confidence columns entirely -filtered = kalign.filter_alignment(result, threshold=0.7) -# filtered.sequences are shorter — only high-confidence columns remain - -# align_file_to_file gains optional confidence args -kalign.align_file_to_file( - "in.fa", "out.fa", - mode="accurate", - confidence_threshold=0.7, - confidence_style="lowercase", # or "remove" -) - -# Write raw confidence scores -kalign.write_confidence("confidence.txt", result) -``` - -### Implementation - -**C library changes:** - -1. `lib/src/msa_io.c` — modify `write_fasta` / `write_clustal` / `write_msf` to apply masking: - - Accept threshold + style parameters - - Before writing each character: if `col_confidence[col] < threshold`, apply style - - For "lowercase": `seq[col] = tolower(seq[col])` (only if not gap) - - For "remove": `seq[col] = '-'` - -2. `src/run_kalign.c` — add CLI flags: - - `--confidence-threshold` → `float conf_threshold` - - `--confidence-style` → `enum {CONF_LOWERCASE, CONF_REMOVE}` - - `--confidence-output` → write `msa->col_confidence[]` to file - - Validate: if threshold > 0 and mode is not ensemble, warn and ignore - -3. `python-kalign/__init__.py` — add `mask_alignment()`, `filter_alignment()`, `write_confidence()` -4. `python-kalign/_core.cpp` — pass threshold/style to `kalign_write_msa` or post-process in Python - -**Key constraint:** Confidence is only available after ensemble alignment (`col_confidence` is NULL for single-run modes). The CLI and Python API must handle this gracefully — warn if the user requests masking with fast/default mode. - -### Testing - -1. **Unit test:** Align BB11001 with accurate mode, verify `col_confidence` is populated, apply threshold=0.5, check that output has lowercase characters in the right columns - -2. **Round-trip test:** Write masked alignment, read it back, verify non-masked residues are unchanged - -3. **Quality test (for paper):** Score all 218 BAliBASE cases: - - Compute SP and TC on ALL columns (standard) - - Compute SP and TC on ONLY columns with confidence >= threshold - - Show improvement as threshold increases - - Script: `scripts/bench_confidence_filtering.py` - -4. **Phylogenetic test (for paper):** Use the manuscript's INDELible simulations: - - Align with kalign accurate - - Filter at various thresholds - - Run IQ-TREE - - Compare RF distance to true tree - - Already partially done in the manuscript pipeline - ---- - -## Feature 2: Add sequences to existing alignment - -### What - -Align new sequences against an existing (fixed) alignment without modifying the existing sequences. Each new sequence is independently aligned to the consensus profile of the existing alignment. - -### Why - -This is one of the most requested features in MSA tools. MAFFT `--add` is heavily cited specifically for this. Use cases: -- Metagenomics: add new sample sequences to a reference alignment -- Phylogenetics: add new taxa to a growing tree alignment -- Viral surveillance: daily additions to reference alignments (SARS-CoV-2, influenza) -- Database maintenance: adding sequences to curated family alignments - -### Paper angle - -**Benchmark against MAFFT --add:** - -1. Take BAliBASE reference alignments. For each case: - - Hold out 20% of sequences as "new" - - Use remaining 80% as the "existing alignment" - - Add the held-out sequences with kalign --add and mafft --add - - Score the added sequences against the full reference alignment - - Measure time - -2. Larger-scale test with simulated data: - - INDELible simulation with 500 sequences - - Use first 400 as reference alignment (align normally) - - Add remaining 100 with --add - - Compare to full 500-sequence alignment - - Time comparison: kalign --add vs mafft --add - -This gives both quality and speed comparisons. Kalign should be faster (SIMD Hirschberg). Quality depends on how well the seq-to-profile alignment places gaps. - -### C CLI interface - -``` -kalign --add new_seqs.fa --existing aligned.fa -o combined.fa - -Options: - --add FILE Unaligned sequences to add to an existing alignment - --existing FILE Existing alignment (FASTA/MSF/Clustal). These sequences - are NOT re-aligned — their gaps are preserved exactly. - -o FILE Output: existing sequences (unchanged) + new sequences - (with gaps inserted to fit the existing column structure) - --nthreads N Parallel seq-to-profile alignments for each new sequence -``` - -**Behavior:** -- Read existing alignment → build profile -- For each new sequence: align to profile, insert gaps -- Output: existing sequences verbatim + new sequences with gaps -- Existing sequences are NEVER modified -- Column count may increase if new sequences have insertions not present in the existing alignment (new gap columns inserted in ALL sequences at those positions) - -### Python API - -```python -# From files -kalign.add_to_alignment( - existing="reference.fa", - new_sequences="new.fa", - output="combined.fa", - n_threads=8, -) - -# In-memory -existing = kalign.align(ref_sequences, mode="accurate") -combined = kalign.add_sequences(existing, new_sequences, n_threads=8) -# combined.sequences = existing (unchanged) + new (gapped) -# combined.names = existing names + new names -``` - -### Implementation - -**C library:** - -1. **New public API function in `kalign.h`:** - ```c - int kalign_add_sequences(struct msa* existing_aln, - struct msa* new_seqs, - int n_threads); - ``` - - `existing_aln`: finalized alignment (sequences have gap chars, alnlen set) - - `new_seqs`: unaligned sequences - - After call: `existing_aln` contains original + new sequences, all aligned - - Returns OK/FAIL - -2. **Core algorithm in new file `lib/src/aln_add.c`:** - - ``` - kalign_add_sequences(existing, new_seqs, n_threads): - 1. Detect biotype, encode new sequences to internal alphabet - 2. Build consensus profile from existing alignment - - Walk all columns of existing alignment - - At each column: count residue frequencies (weighted by sequence count) - - Store as float[64] per column (same format as progressive profiles) - 3. For each new sequence (parallelizable with tp_parallel_for): - a. Run seq-to-profile Hirschberg alignment (aln_seqprofile) - b. Extract gap positions from alignment path - c. Insert gaps into new sequence to match existing column structure - d. If new sequence has insertions not in existing alignment: - - Record insertion positions and lengths - 4. If any insertions were found: - - Insert new gap columns into ALL sequences (existing + new) at those positions - - Update alnlen - 5. Append new sequences to existing MSA - ``` - -3. **Profile building from existing alignment:** - - Similar to what `make_profile_n` does during progressive alignment - - But operates on finalized character sequences with gap chars - - Convert back to internal representation for the DP - - Or: build profile directly from character frequencies per column - -4. **CLI in `src/run_kalign.c`:** - - Parse `--add` and `--existing` flags - - Read both files - - Call `kalign_add_sequences` - - Write combined output - -5. **Python bindings in `_core.cpp`:** - - New `add_sequences` function - - Reads existing alignment file, reads new sequences file, calls C API - -**Parallelism:** Each new sequence's alignment to the profile is independent → `tp_parallel_for` over new sequences. This is embarrassingly parallel and should scale well. - -**Key design decision — handling insertions in new sequences:** - -When a new sequence has residues that don't map to any existing column, we must insert new columns. This affects ALL sequences (existing ones get gaps at those positions). Two approaches: - -- **Strict (MAFFT --add style):** Never insert new columns. New sequence insertions are forced into existing columns or dropped. Existing alignment column count is preserved exactly. -- **Flexible:** Insert new columns as needed. More accurate for the new sequences but modifies the column structure. - -Recommend: **strict mode as default** (existing sequences completely untouched, even column count preserved), with a `--add-insertions` flag for flexible mode. The strict mode is what users expect from "add to existing alignment." - -In strict mode, the seq-to-profile alignment path may indicate insertions in the new sequence. These residues are simply lowercase or dropped (user choice). This matches MAFFT --add behavior. - -### Testing - -1. **Identity test:** Add a sequence that's already in the alignment. Result should be identical to the original. - -2. **Residue preservation test:** After adding, count non-gap characters in each new sequence — must equal original sequence length. - -3. **Existing-unchanged test:** After adding, the existing sequences must be byte-identical to the input existing alignment. - -4. **BAliBASE holdout test (for paper):** - - For each BAliBASE case: hold out 20% sequences - - Align 80% normally → existing alignment - - Add held-out 20% with kalign --add - - Score the full result against BAliBASE reference - - Compare to: MAFFT --add, and full kalign alignment of all sequences - -5. **Speed test (for paper):** - - 1000-sequence reference alignment + add 100 new sequences - - Time kalign --add vs mafft --add - - Repeat at 5000 + 500, 10000 + 1000 - -6. **Simulation test (for paper):** - - INDELible: generate true alignment of 500 sequences - - Split: 400 reference + 100 to add - - Align 400 with each tool - - Add 100 with each tool's --add - - Score against true alignment - - Also run full 500-sequence alignment as upper bound - ---- - -## Implementation order - -``` -Phase 1: Confidence masking (simpler, builds on existing infrastructure) - 1a. C library: masking function in msa_op.c - 1b. CLI: --confidence-threshold, --confidence-style, --confidence-output - 1c. Python: mask_alignment(), filter_alignment(), write_confidence() - 1d. Tests: unit + BAliBASE quality at thresholds - 1e. Paper benchmark: SP/TC on confident columns only - -Phase 2: Add sequences (more complex, new alignment mode) - 2a. C library: kalign_add_sequences in aln_add.c - 2b. Profile building from existing alignment - 2c. Seq-to-profile alignment + gap insertion - 2d. CLI: --add, --existing - 2e. Python: add_to_alignment(), add_sequences() - 2f. Tests: identity + preservation + unchanged + holdout - 2g. Paper benchmark: BAliBASE holdout + speed vs MAFFT --add -``` - -## Estimated complexity - -| Component | Lines of C | Difficulty | -|-----------|:----------:|:----------:| -| Confidence masking (msa_op.c) | ~50 | Easy | -| Confidence CLI flags | ~30 | Easy | -| Confidence Python API | ~80 | Easy | -| Confidence paper benchmark script | ~150 | Easy | -| Add-sequences core (aln_add.c) | ~300 | Medium | -| Add-sequences profile builder | ~150 | Medium | -| Add-sequences CLI | ~50 | Easy | -| Add-sequences Python API | ~100 | Easy | -| Add-sequences paper benchmark | ~200 | Medium | diff --git a/docs/PRD-msa-consistency.md b/docs/PRD-msa-consistency.md deleted file mode 100644 index 122aaa9..0000000 --- a/docs/PRD-msa-consistency.md +++ /dev/null @@ -1,101 +0,0 @@ -# PRD: MSA-Derived Consistency Merge for Ensemble Alignment - -## Goal - -Add an alternative ensemble merge strategy that extracts residue-residue -consistency scores from N completed ensemble MSAs and uses them as bonus -weights in a final progressive alignment, replacing the POAR consensus path. - -## Background - -The current ensemble pipeline: -1. Runs N progressive alignments with diverse parameters -2. Builds a POAR table, scores each run, picks the best -3. Either selects the best single run or builds a POAR consensus MSA - -This works well for F1 (0.810) but TC lags (0.523 vs 0.58+ for MUSCLE/MAFFT). -TC measures column-level consistency — exactly what a consistency-transformed -re-alignment should improve. - -The idea: instead of POAR consensus, extract pairwise residue correspondences -from the N ensemble MSAs (which residues land in the same column across -multiple runs?) and use those agreement counts as bonus scores during a fresh -progressive alignment. The best-scoring run's gap penalties and matrix are -reused for the final alignment — no extra parameters to optimize. - -## Design - -### Reuse `consistency_table` + `sparse_bonus` - -The existing anchor consistency system provides: -- `consistency_table` — stores position maps indexed by [seq × K + slot] -- `sparse_bonus` — sparse bonus matrix queried during DP -- `anchor_consistency_get_bonus()` / `_get_bonus_profile()` — computes - bonuses for seq-seq, seq-profile, and profile-profile merges -- DP integration — `sparse_bonus_lookup()` already wired into match scores - -For MSA consistency, K = n_runs, and position maps come from MSA column -structure instead of pairwise alignments. The "anchor position" is replaced -by "column index" — the `get_bonus` functions work unchanged. - -### Data flow (when `consistency_merge = 1`) - -``` -1. Run N alignments with diverse params [unchanged] -2. Extract POARs from each alignment [unchanged] -3. Score alignments, find best_k [unchanged] -4. NEW: Build consistency_table from N aligned MSAs -5. NEW: Copy original MSA, attach consistency_table -6. NEW: Run progressive alignment with best_k's params + consistency bonus -7. Copy result back to original MSA -8. Compute per-residue confidence from POAR [unchanged] -``` - -### Column map extraction - -For each ensemble MSA k, for each sequence i, build: -``` -col_map[pos] = column index in aligned MSA k -``` -where `pos` is the ungapped residue position. Stored in -`consistency_table.pos_maps[seq_i * K + run_k]`. - -When queried for pair (seq_a, seq_b): if both map to the same column in -run k, that's a vote. Bonus = weight × (votes / K). - -## API Changes - -### C: `kalign_ensemble_config` -- `int consistency_merge;` — 0 = POAR (default), 1 = MSA consistency -- `float consistency_merge_weight;` — bonus weight (default 2.0) - -### Python: `ensemble_custom_file_to_file()` -- `consistency_merge` (int, default 0) -- `consistency_merge_weight` (float, default 2.0) - -### Optimizer search space - -The NSGA-III optimizer that derived the preset values now lives in the -manuscript repository (`scripts/optimizers/`); the search space for this -feature was: -- `consistency_merge`: Choice({0, 1}), only when n_runs > 1 -- `consistency_merge_weight`: Real([0.5, 10.0]), only when consistency_merge=1 - -## Weight Parameter - -A weight parameter is needed. The bonus is added to the substitution score -in the DP. Too high → rigidly reproduces ensemble consensus. Too low → no -benefit. The optimizer finds the sweet spot. Default 2.0. - -## Implementation - -### New files -- `lib/src/msa_consistency.c` — `msa_consistency_build()` -- `lib/src/msa_consistency.h` — header - -### Modified files -- `lib/include/kalign/kalign_config.h` — ensemble_config fields -- `lib/src/aln_wrap.c` — defaults function -- `lib/src/ensemble.c` — consistency merge path -- `lib/CMakeLists.txt` — new source file -- `python-kalign/_core.cpp` — expose params diff --git a/docs/PRD-parameter-cleanup.md b/docs/PRD-parameter-cleanup.md deleted file mode 100644 index 782ddd5..0000000 --- a/docs/PRD-parameter-cleanup.md +++ /dev/null @@ -1,542 +0,0 @@ -# PRD: Kalign Parameter Architecture Cleanup - -> **Status note:** this document captures the original three-mode design -> (`fast` / `default` / `accurate`). A fourth mode, `recall`, was added -> during implementation. The architecture below is otherwise current; -> mentally substitute "four modes × three biotypes" for the preset grid. - -## Goal - -Replace the current parameter specification system — which conflates sequence -types with matrix choices, uses sentinel values resolved at multiple depths, and -exposes internals to users — with a clean two-layer design: - -1. **User layer**: one parameter (`mode`) that selects a fully optimised preset. -2. **Engine layer**: one function (`kalign_align_full`) that takes an array of - fully concrete run configurations. - -The optimizer populates the engine layer directly. Users never see it. - ---- - -## 1. User-Facing API - -### Python - -```python -# The only parameter most users ever need: -result = kalign.align(sequences, mode="default") -result = kalign.align(sequences, mode="fast") -result = kalign.align(sequences, mode="accurate") - -# Force biotype when auto-detection is wrong (rare): -result = kalign.align(sequences, mode="default", seq_type="protein") - -# Threading: -result = kalign.align(sequences, mode="default", n_threads=4) -``` - -### Parameters exposed to users - -| Parameter | Type | Values | Default | -|------------------------|---------------|-------------------------------------|-------------| -| `mode` | str | `"fast"`, `"default"`, `"accurate"` | `"default"` | -| `seq_type` | str | `"auto"`, `"dna"`, `"rna"`, `"protein"` | `"auto"` | -| `n_threads` | int | >= 1 | 1 | -| `gap_open` | float or None | penalty value | None | -| `gap_extend` | float or None | penalty value | None | -| `terminal_gap_extend` | float or None | penalty value | None | - -Mode handles everything for the common case. The gap penalty parameters -exist for backward compatibility and expert use. - -### Gap penalty override rule - -When the user provides **any** gap penalty (`gap_open`, `gap_extend`, or -`terminal_gap_extend`), the following happens: - -1. The `mode` parameter is **ignored** (regardless of what was passed). -2. The `fast` preset for the detected biotype is loaded (single run). -3. The user's gap penalties **replace** the corresponding preset values. -4. Unspecified penalties keep the `fast` preset defaults. - -This gives expert users direct control over gap scoring while keeping -everything else (matrix, VSM, seq_weights, etc.) at sensible optimised -values. - -```python -# Uses default mode (5-run ensemble): -kalign.align(sequences) - -# Uses fast preset, overrides gap open only: -kalign.align(sequences, gap_open=5.0) - -# Uses fast preset, overrides all three: -kalign.align(sequences, gap_open=5.0, gap_extend=0.5, - terminal_gap_extend=0.3) - -# mode is ignored when gap penalties are set: -kalign.align(sequences, mode="accurate", gap_open=5.0) # → fast + gpo=5.0 -``` - -### File-based variants - -```python -# Read from file, return AlignedSequences (names + sequences) -result = kalign.align_from_file("input.fasta", mode="default") - -# Read from file, write to file -kalign.align_file_to_file("input.fasta", "output.fasta", - mode="default", format="fasta") -``` - -`align_file_to_file` adds one extra parameter: - -| Parameter | Type | Values | Default | -|-----------|------|---------------------------------|-----------| -| `format` | str | `"fasta"`, `"msf"`, `"clu"` | `"fasta"` | - -### Output formats - -- `align()` returns `List[str]` (aligned sequences) or ecosystem objects - via `fmt="biopython"` / `fmt="skbio"`. -- `align_from_file()` returns `AlignedSequences` (names + sequences + - optional confidence). -- `align_file_to_file()` writes to disk, returns nothing. - -### CLI - -```bash -kalign -i input.fasta -o output.fasta # mode=default -kalign -i input.fasta -o output.fasta --mode fast -kalign -i input.fasta -o output.fasta --mode accurate -kalign -i input.fasta -o output.fasta --type protein # force biotype -kalign -i input.fasta -o output.fasta --gpo 5.0 # fast + gpo override -``` - ---- - -## 2. Mode Presets - -Each mode is a static lookup table of fully concrete run configurations. The -number of runs, the matrix per run, the gap penalties per run, the tree seed -per run — everything is baked in. No computation, no scaling, no indirection. - -Presets are **biotype-specific**: protein, DNA, and RNA each have their own -optimised configurations. - -### Mode semantics - -Three modes trade speed against accuracy. The user's only decision is where -on that tradeoff they want to be. - -| Mode | Intent | -|------------|-----------------------------------------------------| -| `fast` | Fastest possible alignment, acceptable quality | -| `default` | Good balance of speed and accuracy for routine use | -| `accurate` | Best achievable accuracy, speed secondary | - -### Preset grid - -There are **9 preset slots**: 3 modes × 3 biotypes. - -| | Protein | DNA | RNA | -|------------|---------|-----|-----| -| `fast` | slot | slot| slot| -| `default` | slot | slot| slot| -| `accurate` | slot | slot| slot| - -Each slot is filled by the optimizer independently. The number of runs, -the choice of matrices, the gap penalties, whether realignment is used — -all of this is determined per slot by multi-objective optimization, not -prescribed by the PRD. - -Constraints on the optimizer: -- `n_runs` must be 1, 3, or 5 (no other ensemble sizes). -- `fast` should be Pareto-optimal for speed; `accurate` for quality; - `default` for balanced. -- Within each biotype, `fast` must be strictly faster than `default`, - and `default` strictly faster than `accurate`. - -### Preset structure (per mode × biotype) - -Each preset specifies: - -**Ensemble-level parameters** (structural — not per-run): - -| Field | Description | -|----------------|-------------------------------------------------| -| `n_runs` | Number of alignment runs (1, 3, or 5) | -| `min_support` | POAR consensus column threshold (ensemble only) | - -These are inherently about the ensemble as a whole, not about any -individual alignment run. - -**Per-run parameters** (each run in the ensemble has its own values): - -| Field | Description | -|----------------|-------------------------------------------------| -| `matrix` | Substitution matrix for this run | -| `gpo` | Gap open penalty | -| `gpe` | Gap extend penalty | -| `tgpe` | Terminal gap extend penalty | -| `vsm_amax` | Variable scoring matrix amplitude (0 = off) | -| `seq_weights` | Profile rebalancing pseudo-count (0 = off) | -| `dist_scale` | Distance-dependent gap scaling (0 = off) | -| `realign` | Alignment-guided tree-rebuild iterations (0+) | -| `refine` | Post-alignment refinement strategy | -| `adaptive_budget` | Scale refinement trials by uncertainty (0=off)| -| `tree_seed` | RNG seed for guide tree construction | -| `tree_noise` | Guide tree perturbation sigma | - -The optimizer may choose identical values across runs for some -parameters (e.g. the same `vsm_amax` for all 5 runs) or vary them -(e.g. `realign=2` on run 1 but `realign=0` on run 3). That is the -optimizer's decision, not an architectural constraint. - -This is the **complete parameter space** exposed to the optimizer. - ---- - -## 3. Engine Layer (C API) - -### Substitution matrix constants - -```c -#define KALIGN_MATRIX_AUTO 0 /* auto-select for biotype */ -#define KALIGN_MATRIX_PFASUM43 1 /* 1/3 bit, divergent protein */ -#define KALIGN_MATRIX_PFASUM60 2 /* 1/3 bit, moderate protein */ -#define KALIGN_MATRIX_CORBLOSUM66 3 /* 1/3 bit, close protein */ -#define KALIGN_MATRIX_DNA 4 /* DNA match/mismatch (+5/-4) */ -#define KALIGN_MATRIX_DNA_INTERNAL 5 /* DNA internal (tgpe=8) */ -#define KALIGN_MATRIX_RNA 6 /* RNA RIBOSUM-like (~160-383) */ -``` - -Every value maps to exactly one scoring table. No duplicates. - -GONNET (gon250) remains in the source as dead code but is not assigned a -constant and is unreachable through any API. - -### Matrix default penalties - -Each matrix has intrinsic default gap penalties. These are used as starting -points when constructing configs, never resolved at alignment time. - -| Matrix | gpo | gpe | tgpe | Score range | -|---------------|--------|-------|--------|---------------| -| PFASUM43 | 7.0 | 1.25 | 1.0 | -6 to 13 | -| PFASUM60 | 7.0 | 1.25 | 1.0 | -6 to 14 | -| CorBLOSUM66 | 5.5 | 2.0 | 1.0 | -4 to 13 | -| DNA | 8.0 | 6.0 | 0.0 | -4 to 5 | -| DNA_INTERNAL | 8.0 | 6.0 | 8.0 | -4 to 5 | -| RNA | 217.0 | 39.4 | 292.6 | ~160 to 383 | - -PFASUM43, PFASUM60, and CorBLOSUM66 are all in 1/3-bit units. Their gap -penalties are directly comparable. A penalty value like `gpo=7.0` means the -same thing across all three matrices. - -### Refinement constants - -```c -#define KALIGN_REFINE_NONE 0 /* no post-alignment refinement */ -#define KALIGN_REFINE_ALL 1 /* refine all columns (two-pass) */ -#define KALIGN_REFINE_CONFIDENT 2 /* refine high-confidence columns */ -#define KALIGN_REFINE_INLINE 3 /* per-node refinement during tree */ -``` - -### Run config struct - -```c -struct kalign_run_config { - int matrix; /* KALIGN_MATRIX_* */ - float gpo; /* gap open penalty (concrete, no sentinel) */ - float gpe; /* gap extend penalty */ - float tgpe; /* terminal gap extend penalty */ - float vsm_amax; /* variable scoring matrix amplitude (0=off) */ - float seq_weights; /* profile rebalancing pseudo-count (0=off) */ - float dist_scale; /* distance-dependent gap scaling (0=off) */ - int refine; /* KALIGN_REFINE_* */ - int adaptive_budget; /* scale refinement by uncertainty (0=off) */ - int realign; /* tree-rebuild iterations (0=none) */ - uint64_t tree_seed; /* guide tree RNG seed */ - float tree_noise; /* guide tree perturbation sigma (0=none) */ -}; -``` - -No sentinel values. Every field is a concrete, usable value. A config -obtained from `kalign_run_config_defaults()` or `kalign_get_mode_preset()` -is directly usable without further resolution. - -### Ensemble config struct - -```c -struct kalign_ensemble_config { - int min_support; /* POAR consensus column threshold */ -}; -``` - -Simplified from current struct. `seed` removed (each run has its own -`tree_seed`). `save_poar` removed (debug feature, not part of core API). - -### Preset function - -```c -int kalign_get_mode_preset(const char *mode, - int biotype, - struct kalign_run_config *runs, - int *n_runs, - struct kalign_ensemble_config *ens); -``` - -- `mode`: `"fast"`, `"default"`, `"accurate"` (case-insensitive). -- `biotype`: `ALN_BIOTYPE_PROTEIN`, `ALN_BIOTYPE_DNA`, or `ALN_BIOTYPE_RNA`. - Determined by auto-detection before this call. -- `runs`: caller-allocated array (minimum 8 elements). -- `n_runs`: filled with the number of runs in the preset. -- `ens`: filled with ensemble config. -- Returns 0 on success, -1 on unknown mode. - -### Alignment entry point - -```c -int kalign_align_full(struct msa *msa, - const struct kalign_run_config *runs, - int n_runs, - const struct kalign_ensemble_config *ens, - int n_threads); -``` - -This is the single entry point for all alignment. It receives fully concrete -configs and executes them. No parameter resolution, no sentinel handling. - -If `n_runs == 1`: single alignment using `runs[0]`. -If `n_runs > 1`: ensemble — run each config, build POAR, consensus. - -When `runs[k].matrix == KALIGN_MATRIX_AUTO`: -- If `msa->biotype == DNA`: resolves to `KALIGN_MATRIX_DNA`, uses DNA - default penalties (overriding the config's gpo/gpe/tgpe). -- If `msa->biotype == RNA`: resolves to `KALIGN_MATRIX_RNA`, uses RNA - default penalties. -- If `msa->biotype == protein`: resolves to PFASUM43 or PFASUM60 using - the length-ratio heuristic (ratio < 1.5 → PFASUM43, else PFASUM60). - Keeps the config's gpo/gpe/tgpe. - -This is the **only** place where AUTO is resolved, and it happens **once** -before the alignment loop. - ---- - -## 4. Parameters Exposed to the Optimizer - -The optimizer sees the engine layer directly. It constructs -`kalign_run_config` arrays and calls `kalign_align_full`. - -### Optimizer search space - -**Ensemble-level parameters** (one value per preset slot): - -| Parameter | Type | Range | Description | -|----------------|--------|--------------------|--------------------------------| -| `n_runs` | int | {1, 3, 5} | Number of ensemble runs | -| `min_support` | int | [0, n_runs] | POAR consensus threshold | - -**Per-run parameters** (optimised independently for each of the n_runs): - -| Parameter | Type | Range | Description | -|----------------|--------|--------------------|--------------------------------| -| `matrix` | int | {1, 2, 3} | PFASUM43, PFASUM60, CorBLOSUM66| -| `gpo` | float | [1.0, 20.0] | Gap open penalty | -| `gpe` | float | [0.1, 5.0] | Gap extend penalty | -| `tgpe` | float | [0.1, 5.0] | Terminal gap extend penalty | -| `vsm_amax` | float | [0.0, 3.0] | VSM amplitude (0 = disabled) | -| `seq_weights` | float | [0.0, 3.0] | Pseudo-count (0 = disabled) | -| `dist_scale` | float | [0.0, 2.0] | Distance gap scaling (0 = off) | -| `realign` | int | {0, 1, 2, 3} | Tree-rebuild iterations | -| `refine` | int | {0, 1, 2, 3} | Refinement strategy | -| `adaptive_budget` | int | {0, 1} | Scale refinement by uncertainty| -| `tree_seed` | uint64 | fixed per run index | Deterministic seed | -| `tree_noise` | float | [0.0, 0.5] | Tree perturbation | - -The matrix values {1, 2, 3} correspond to the three protein matrices on -the same 1/3-bit scale, so a single set of gap penalty ranges works for -all of them. - -For DNA/RNA optimisation, the matrix field is fixed (DNA or RNA) and the -penalty ranges are adjusted to match those matrices' scales. - -The optimizer may choose to use the same value for a parameter across all -runs (e.g. `vsm_amax=2.0` for every run) or vary it per run. That is a -search strategy decision, not an architectural constraint. - -### What the optimizer produces - -A JSON file with this structure: - -```json -{ - "protein": { - "fast": { - "n_runs": 1, - "min_support": 0, - "runs": [ - { - "matrix": "pfasum60", - "gpo": 8.4087, - "gpe": 0.5153, - "tgpe": 0.4927, - "vsm_amax": 1.448, - "seq_weights": 1.063, - "dist_scale": 0.0, - "realign": 0, - "refine": 0, - "adaptive_budget": 0, - "tree_seed": 42, - "tree_noise": 0.1623 - } - ] - }, - "default": { - "n_runs": 5, - "min_support": 3, - "runs": [ { ... }, { ... }, { ... }, { ... }, { ... } ] - }, - "accurate": { - "n_runs": 5, - "min_support": 3, - "runs": [ { ... }, { ... }, { ... }, { ... }, { ... } ] - } - }, - "dna": { - "fast": { ... }, - "default": { ... }, - "accurate": { ... } - }, - "rna": { - "fast": { ... }, - "default": { ... }, - "accurate": { ... } - } -} -``` - -Each run object contains every per-run parameter. The JSON maps 1:1 to the -C preset tables. No interpretation, no transformation. The values in the -JSON are the values used in alignment. - ---- - -## 5. Removed Parameters - -The following are **removed from the user-facing API**. They remain -accessible only through the engine layer (`kalign_align_full`) for -benchmarking and optimiser use. - -| Removed parameter | Reason | -|------------------------|-----------------------------------------------| -| `ensemble` | Set by mode preset (1, 3, or 5) | -| `ensemble_seed` | Replaced by per-run `tree_seed` | -| `matrix` / `seq_type` | Set by mode preset per run | -| `vsm_amax` | Set by mode preset | -| `seq_weights` | Set by mode preset | -| `realign` | Set by mode preset | -| `refine` | Set by mode preset | -| `min_support` | Set by mode preset | -| `consistency` | Set by mode preset (currently always 0) | -| `consistency_weight` | Set by mode preset (dead when consistency=0) | -| `dist_scale` | Set by mode preset | -| `adaptive_budget` | Set by mode preset | -| `save_poar` | Debug feature, not user-facing | -| `load_poar` | Debug feature, not user-facing | - ---- - -## 6. Backward Compatibility - -### C header - -Old `KALIGN_TYPE_*` constants become `#define` aliases: - -```c -/* Deprecated — use KALIGN_MATRIX_* */ -#define KALIGN_TYPE_DNA KALIGN_MATRIX_DNA -#define KALIGN_TYPE_DNA_INTERNAL KALIGN_MATRIX_DNA_INTERNAL -#define KALIGN_TYPE_RNA KALIGN_MATRIX_RNA -#define KALIGN_TYPE_PROTEIN KALIGN_MATRIX_PFASUM43 -#define KALIGN_TYPE_PROTEIN_PFASUM43 KALIGN_MATRIX_PFASUM43 -#define KALIGN_TYPE_PROTEIN_PFASUM60 KALIGN_MATRIX_PFASUM60 -#define KALIGN_TYPE_PROTEIN_PFASUM_AUTO KALIGN_MATRIX_AUTO -#define KALIGN_TYPE_UNDEFINED KALIGN_MATRIX_AUTO -#define KALIGN_TYPE_PROTEIN_CORBLOSUM66 KALIGN_MATRIX_CORBLOSUM66 -``` - -`KALIGN_TYPE_PROTEIN_DIVERGENT` gets no alias — it mapped to GONNET -which is dead code. Any code using it gets a compile error, directing -the author to pick a specific matrix. - -Old functions (`kalign_run`, `kalign_run_seeded`, `kalign_ensemble`, -etc.) remain as thin wrappers that build a config and call -`kalign_align_full`. They accept -1.0 sentinels for backward compat, -resolving them into the config immediately. New code should not use them. - -### Python - -`align()`, `align_from_file()`, and `align_file_to_file()` continue to -accept all current keyword arguments during a deprecation period. When -old-style parameters are used, a `DeprecationWarning` is raised. The -old parameters are resolved into a run config and passed to the engine. - -After the deprecation period, the signatures shrink to: - -```python -def align(sequences, *, mode="default", seq_type="auto", - gap_open=None, gap_extend=None, terminal_gap_extend=None, - n_threads=None, fmt="plain", ids=None) - -def align_from_file(input_file, *, mode="default", seq_type="auto", - gap_open=None, gap_extend=None, - terminal_gap_extend=None, n_threads=None) - -def align_file_to_file(input_file, output_file, *, mode="default", - seq_type="auto", gap_open=None, gap_extend=None, - terminal_gap_extend=None, format="fasta", - n_threads=None) -``` - -Gap penalty parameters are retained permanently (not deprecated). When -any is set, the gap penalty override rule applies: mode is ignored, the -`fast` preset is loaded, and user penalties replace preset defaults. - ---- - -## 7. Implementation Order - -1. **Add `KALIGN_MATRIX_*` constants** to `kalign.h`. Add backward-compat - aliases for `KALIGN_TYPE_*`. Add `biotype` parameter to - `kalign_get_mode_preset`. - -2. **Rename `type` → `matrix`** in `kalign_run_config`. Update all code - that reads the field. - -3. **Remove sentinels from the new path.** `kalign_run_config_defaults()` - returns concrete PFASUM43 protein values. `aln_param_init` uses values - directly (no `if(gpo >= 0)` guard). Old functions keep sentinel - resolution internally. - -4. **Fix v2 presets.** Change all `KALIGN_TYPE_PROTEIN_DIVERGENT` → - `KALIGN_MATRIX_PFASUM43` in `kalign_get_mode_preset()`. These runs - were always PFASUM43; the "gonnet" label was a bug. - -5. **Add DNA/RNA preset stubs** in `kalign_get_mode_preset()`. Initially - use matrix defaults; optimise later. - -6. **Simplify Python API.** New signatures with `mode` + `seq_type` + - `n_threads`. Old parameters accepted with deprecation warnings. - Remove `_MODE_PRESETS` dict and `-1.0` sentinel logic from Python. - -7. **Update optimizer** to use `KALIGN_MATRIX_*` constants and produce - the JSON format described in section 4. - -8. **Benchmark** all three modes × all three biotypes. Verify protein - results match optimizer predictions now that the matrix bug is fixed. - -9. **Remove deprecated parameters** after one release cycle. diff --git a/docs/parameter-cleanup-integration.md b/docs/parameter-cleanup-integration.md deleted file mode 100644 index 6876e6e..0000000 --- a/docs/parameter-cleanup-integration.md +++ /dev/null @@ -1,337 +0,0 @@ -# Kalign Python API: Integration Guide - -This document describes kalign's Python API architecture and how to use -it from benchmark runners and the NSGA-III optimizer. - ---- - -## 1. Architecture: Two Paths - -Kalign's Python bindings have exactly two code paths: - -### Path 1: Mode-based (public API) - -For standard alignment using preset configurations: - -```python -import kalign - -# In-memory -result = kalign.align(sequences, mode="default") -result = kalign.align(sequences, mode="fast") -result = kalign.align(sequences, mode="accurate") - -# File-based (returns AlignedSequences with names + sequences) -result = kalign.align_from_file("input.fasta", mode="default") - -# File-to-file -kalign.align_file_to_file("input.fasta", "output.fasta", mode="default") -``` - -These call `_core.align_mode()` / `_core.align_from_file_mode()` / -`_core.align_file_to_file_mode()`, which delegate to the C function -`kalign_get_mode_preset()` → `kalign_align_full()`. - -**Mode presets** are biotype-aware. Each mode × biotype (protein / DNA / RNA) -returns fully concrete per-run configurations optimized by NSGA-III (protein) -or using sensible defaults (DNA/RNA). - -**Gap penalty override rule**: If any gap penalty (`gap_open`, `gap_extend`, -`terminal_gap_extend`) is set, mode is forced to `"fast"` and the specified -penalties replace the preset values. Value `-1.0` means "use preset default". - -### Path 2: Optimizer (internal API) - -For the NSGA-III optimizer that needs full per-run control: - -```python -from kalign._core import ensemble_custom_file_to_file - -ensemble_custom_file_to_file( - input_file="input.fasta", - output_file="output.fasta", - run_gpo=[8.4, 6.2, 7.1], # per-run gap open - run_gpe=[0.5, 1.0, 0.8], # per-run gap extend - run_tgpe=[0.5, 0.9, 0.6], # per-run terminal gap extend - run_noise=[0.16, 0.25, 0.10], # per-run tree noise sigma - run_types=[1, 2, 3], # per-run matrix constants - format="fasta", - seq_type=1, # KALIGN_MATRIX_PFASUM43 - seed=42, # base seed (run k gets seed+k) - min_support=0, # POAR consensus threshold - refine=2, # KALIGN_REFINE_CONFIDENT - vsm_amax=2.0, # shared across runs - realign=1, # shared across runs - seq_weights=1.0, # shared across runs - n_threads=4, - consistency_anchors=0, # shared across runs - consistency_weight=2.0, # shared across runs -) -``` - -This builds a `kalign_run_config[]` array and calls `kalign_align_full()`. - ---- - -## 2. C Entry Point - -```c -int kalign_align_full(struct msa *msa, - const struct kalign_run_config *runs, - int n_runs, - const struct kalign_ensemble_config *ens, - int n_threads); -``` - -Single entry point for all alignment. Receives fully concrete configs -(no sentinel values) and executes them. - -### `kalign_run_config` (14 fields) - -```c -struct kalign_run_config { - int matrix; /* KALIGN_MATRIX_* constant */ - float gpo; /* gap open penalty */ - float gpe; /* gap extend penalty */ - float tgpe; /* terminal gap extend penalty */ - float vsm_amax; /* variable scoring matrix amp (0=off) */ - float seq_weights; /* profile rebalancing (0=off) */ - float dist_scale; /* distance-dependent gap scaling */ - int refine; /* KALIGN_REFINE_* constant */ - int adaptive_budget; /* scale refinement by uncertainty */ - int realign; /* iterative tree-rebuild iters (0=off) */ - uint64_t tree_seed; /* random seed for tree perturbation */ - float tree_noise; /* tree perturbation sigma (0.0=none) */ - int consistency_anchors; /* anchor consistency rounds (0=off) */ - float consistency_weight; /* anchor consistency bonus (default 2) */ -}; -``` - -### `kalign_ensemble_config` - -```c -struct kalign_ensemble_config { - int min_support; /* POAR consensus threshold (0=auto) */ -}; -``` - -### Mode presets - -```c -int kalign_get_mode_preset(const char *mode, /* "fast"/"default"/"accurate" */ - int biotype, /* ALN_BIOTYPE_PROTEIN or _DNA */ - struct kalign_run_config *runs, /* out: array[8] */ - int *n_runs, /* out */ - struct kalign_ensemble_config *ens); /* out */ -``` - ---- - -## 3. Matrix Constants - -| Constant | Value | Description | -|---------------------------|-------|---------------------| -| `KALIGN_MATRIX_AUTO` | 0 | Auto-detect | -| `KALIGN_MATRIX_PFASUM43` | 1 | PFASUM43 (protein) | -| `KALIGN_MATRIX_PFASUM60` | 2 | PFASUM60 (protein) | -| `KALIGN_MATRIX_CORBLOSUM66`| 3 | CorBLOSUM66 (protein) | -| `KALIGN_MATRIX_DNA` | 5 | DNA | -| `KALIGN_MATRIX_DNA_INTERNAL`| 6 | DNA internal | -| `KALIGN_MATRIX_RNA` | 7 | RNA | - -Python access: -```python -from kalign._core import ( - MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66, - MATRIX_DNA, MATRIX_DNA_INTERNAL, MATRIX_RNA, MATRIX_AUTO, -) -``` - -Legacy names (`PROTEIN`, `DNA`, etc.) still work as aliases. - -### Critical bug fix: value 3 was GONNET, now CorBLOSUM66 - -Old code used `PROTEIN` (value 3) thinking it was a generic protein -matrix, but it actually triggered the GONNET scoring path (gon250: -scores ~0-200). This is now `MATRIX_CORBLOSUM66` (CorBLOSUM66, -1/3-bit units, scores -4 to 13). - -The correct three-matrix mapping for protein optimization: - -```python -from kalign._core import MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66 - -matrix_map_int = [MATRIX_PFASUM43, MATRIX_PFASUM60, MATRIX_CORBLOSUM66] -matrix_map_str = ["pfasum43", "pfasum60", "corblosum66"] -``` - ---- - -## 4. Python API Signatures - -### `kalign.align()` - -```python -kalign.align( - sequences: list[str], - seq_type: str | int = "auto", # "auto", "dna", "rna", "protein" - gap_open: float | None = None, # overrides mode → forces "fast" - gap_extend: float | None = None, - terminal_gap_extend: float | None = None, - n_threads: int | None = None, - mode: str | None = None, # "fast", "default", "accurate" - fmt: str = "plain", # "plain", "biopython", "skbio" - ids: list[str] | None = None, -) -> list[str] -``` - -### `kalign.align_from_file()` - -```python -kalign.align_from_file( - input_file: str, - seq_type: str | int = "auto", - gap_open: float | None = None, - gap_extend: float | None = None, - terminal_gap_extend: float | None = None, - n_threads: int | None = None, - mode: str | None = None, -) -> AlignedSequences # unpacks as (names, sequences) -``` - -### `kalign.align_file_to_file()` - -```python -kalign.align_file_to_file( - input_file: str, - output_file: str, - format: str = "fasta", - seq_type: str | int = "auto", - gap_open: float | None = None, - gap_extend: float | None = None, - terminal_gap_extend: float | None = None, - n_threads: int | None = None, - mode: str | None = None, -) -``` - -### `_core.ensemble_custom_file_to_file()` (optimizer only) - -```python -from kalign._core import ensemble_custom_file_to_file - -ensemble_custom_file_to_file( - input_file: str, - output_file: str, - run_gpo: list[float], # per-run - run_gpe: list[float], # per-run - run_tgpe: list[float], # per-run - run_noise: list[float], # per-run - run_types: list[int] = [], # per-run matrix constants - format: str = "fasta", - seq_type: int = 1, # fallback if run_types empty - seed: int = 42, # base seed (run k → seed+k) - min_support: int = 0, # POAR threshold - refine: int = 0, # KALIGN_REFINE_* constant - vsm_amax: float = -1.0, # -1.0 = C default - realign: int = 0, - seq_weights: float = -1.0, # -1.0 = C default - n_threads: int = 1, - consistency_anchors: int = 0, - consistency_weight: float = 2.0, -) -``` - ---- - -## 5. Optimizer Search Space → `kalign_run_config` Mapping - -``` -Per-run parameter | optimizer field | run_config field -----------------------|----------------------|------------------ -gap open | run_gpo[k] | .gpo -gap extend | run_gpe[k] | .gpe -terminal gap extend | run_tgpe[k] | .tgpe -tree noise | run_noise[k] | .tree_noise -matrix type | run_types[k] | .matrix -tree seed | seed + k | .tree_seed -VSM amplitude | vsm_amax | .vsm_amax -seq weights | seq_weights | .seq_weights -realign iters | realign | .realign -refinement mode | refine | .refine -consistency anchors | consistency_anchors | .consistency_anchors -consistency weight | consistency_weight | .consistency_weight -adaptive budget | (not searched) | .adaptive_budget = 0 -dist scale | (not searched) | .dist_scale = 0.0 -``` - -Ensemble-level: -``` -min_support | min_support | ens.min_support -``` - ---- - -## 6. Refinement Constants - -```python -from kalign._core import REFINE_NONE, REFINE_ALL, REFINE_CONFIDENT, REFINE_INLINE -``` - -| Constant | Value | Description | -|-------------------|-------|----------------------------------------| -| `REFINE_NONE` | 0 | No refinement | -| `REFINE_ALL` | 1 | Refine all columns | -| `REFINE_CONFIDENT` | 2 | Refine only high-confidence columns | -| `REFINE_INLINE` | 3 | Inline refinement during alignment | - ---- - -## 7. Matrix Default Penalties - -| Matrix | gpo | gpe | tgpe | Score range | -|---------------|--------|-------|--------|---------------| -| PFASUM43 | 7.0 | 1.25 | 1.0 | -6 to 13 | -| PFASUM60 | 7.0 | 1.25 | 1.0 | -6 to 14 | -| CorBLOSUM66 | 5.5 | 2.0 | 1.0 | -4 to 13 | -| DNA | 8.0 | 6.0 | 0.0 | -4 to 5 | -| DNA_INTERNAL | 8.0 | 6.0 | 8.0 | -4 to 5 | -| RNA | 217.0 | 39.4 | 292.6 | ~160 to 383 | - -All three protein matrices are in 1/3-bit units with directly comparable -gap penalty ranges (optimizer searches `[2.0, 15.0]` for gpo across all three). - ---- - -## 8. How Presets Get Into the C Library - -Optimized presets are hardcoded in `lib/src/aln_wrap.c` in the functions -`preset_protein()`, `preset_dna()`, and `preset_rna()`. - -Each function handles all three modes (fast/default/accurate) and fills -the caller-provided `runs[]` array and `*n_runs` count. - -To update presets after re-optimization: -1. Run the optimizer to get the Pareto front -2. Select the fast/default/accurate configurations -3. Export to JSON: - ```json - { - "protein": { - "fast": { - "n_runs": 1, - "min_support": 0, - "runs": [{ - "matrix": "pfasum60", - "gpo": 8.4087, "gpe": 0.5153, "tgpe": 0.4927, - "vsm_amax": 1.448, "seq_weights": 1.063, - "dist_scale": 0.0, "realign": 0, "refine": 0, - "adaptive_budget": 0, "tree_seed": 42, "tree_noise": 0.1623, - "consistency_anchors": 0, "consistency_weight": 2.0 - }] - } - } - } - ``` -4. Translate JSON → C `preset_run()` calls in `aln_wrap.c` -5. Rebuild and test From d1ce73cc318bbc69eb9cd209b5411835e93c0f50 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 08:20:07 +0800 Subject: [PATCH 22/29] Harden POAR file parsing against integer overflow Two related fixes in the binary POAR loader (lib/src/poar.c). Both guard against malformed POAR files supplied via --load-poar. 1. Pair-count overflow on numseq >= 65536. The expression `numseq * (numseq - 1) / 2` was evaluated in uint32_t and then cast to int, producing wrap to a negative value for numseq >= 65536 and outright uint32 overflow for numseq >= 65537. Now computed in uint64_t and rejected if it exceeds INT_MAX. 2. Per-pair n_entries unbounded by file. The 32-bit per-pair entry count was cast to int (could wrap negative) and multiplied by sizeof(struct poar_entry) without overflow check. Now capped at INT_MAX / sizeof(struct poar_entry). Verified: ctest 15/15 pass; pytest tests/python/ 170 pass (one pre-existing test_module_exports failure unchanged). --- lib/src/poar.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/src/poar.c b/lib/src/poar.c index 3231375..68d458a 100644 --- a/lib/src/poar.c +++ b/lib/src/poar.c @@ -1,6 +1,7 @@ #include "tldevel.h" #include +#include #include #include @@ -338,7 +339,15 @@ int poar_table_read(struct poar_table** out_table, const char* path) t->pairs = NULL; t->numseq = (int)numseq; t->n_alignments = (int)n_alignments; - t->n_pairs = (int)(numseq * (numseq - 1) / 2); + + /* Compute pair count in 64-bit; numseq*(numseq-1) overflows uint32 at + numseq >= 65537, and the cast to int wraps negative at numseq >= 65536. */ + uint64_t n_pairs64 = (uint64_t)numseq * ((uint64_t)numseq - 1) / 2; + if(n_pairs64 > (uint64_t)INT_MAX){ + ERROR_MSG("POAR file %s: numseq=%u implies %llu pairs, exceeds INT_MAX", + path, numseq, (unsigned long long)n_pairs64); + } + t->n_pairs = (int)n_pairs64; MMALLOC(t->pairs, sizeof(struct poar_pair*) * t->n_pairs); for(i = 0; i < t->n_pairs; i++){ @@ -353,6 +362,13 @@ int poar_table_read(struct poar_table** out_table, const char* path) ERROR_MSG("Failed to read pair %d entries count", i); } + /* Cap so the cast to int doesn't wrap negative and the per-entry + allocation size doesn't overflow size_t. */ + if(n_entries > (uint32_t)(INT_MAX / sizeof(struct poar_entry))){ + ERROR_MSG("POAR file: pair %d has n_entries=%u, exceeds bounds", + i, n_entries); + } + MMALLOC(pp, sizeof(struct poar_pair)); pp->entries = NULL; pp->n_entries = (int)n_entries; From afc18b540cec80d47946e67fbd20629990d2e4cf Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 08:23:57 +0800 Subject: [PATCH 23/29] Fix test_module_exports: add four missing symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hard-coded expected_exports set was stale relative to __all__ in python-kalign/__init__.py — four symbols added in earlier commits weren't reflected here: - add_to_alignment (from 53abded, --add mode) - filter_alignment (from 1002f0a, confidence masking) - mask_alignment (from 1002f0a) - write_confidence (from 1002f0a) Full pytest tests/python/ now passes clean: 171 pass, 0 fail. --- tests/python/test_ecosystem_integration.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/python/test_ecosystem_integration.py b/tests/python/test_ecosystem_integration.py index b0c5b1a..ce06892 100644 --- a/tests/python/test_ecosystem_integration.py +++ b/tests/python/test_ecosystem_integration.py @@ -320,6 +320,10 @@ def test_module_exports(self): "MODE_RECALL", "MODE_ACCURATE", "PROTEIN_CORBLOSUM66", + "add_to_alignment", + "filter_alignment", + "mask_alignment", + "write_confidence", "__version__", "__author__", "__email__", From f7b3dac791c7b27f7ef9c516d58c09d8969568f7 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 08:41:37 +0800 Subject: [PATCH 24/29] Fix Linux build: gate d2 declaration on HAVE_AVX2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier warning-cleanup commit (fb0d5c4) removed `float d2;` from euclidean_dist.c's UTEST_EDIST main(), trusting an Apple Clang -Wunused-variable warning. The warning was correct *for the Apple Silicon build*, where edist_utest is compiled with -DNOHAVE_AVX2 and the `#ifdef HAVE_AVX2` block containing the only use of d2 is preprocessed out. On Linux GCC builds with -DHAVE_AVX2, the block IS compiled and `edist_256(..., &d2)` references the now-missing variable — breaking cmake.yml, benchmark.yml, codeql, and wheels builds. Restore the declaration but guard it with the same `#ifdef HAVE_AVX2` as its only consumer, so neither codepath warns. --- lib/src/euclidean_dist.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/euclidean_dist.c b/lib/src/euclidean_dist.c index dc9b959..b2e76c7 100644 --- a/lib/src/euclidean_dist.c +++ b/lib/src/euclidean_dist.c @@ -33,6 +33,9 @@ int main(void) float** mat = NULL; double r; float d1; +#ifdef HAVE_AVX2 + float d2; +#endif int i,j,c; int max_iter = 10; int num_element = 128; From b09d43faeac6c1cb5c8dc9e2c55694718bbceca4 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 08:47:56 +0800 Subject: [PATCH 25/29] Remove benchmark CI workflow The Benchmark workflow has been silently broken for months because the BAliBASE download endpoint (http://www.lbgi.fr/balibase/...) is gone, and the Wayback Machine archive returns HTML instead of the tarball. Both failure modes are caught and surfaced by datasets.py, but the result is that the workflow has been failing on every push and the github-action-benchmark gh-pages history is stale. Serious benchmark tracking lives in the manuscript repository (~/Work/Documents/Manuscripts/2026_kalign_35/) which runs the full BAliBASE / BaliFam100 / MDSA suite via Snakemake. Removing the broken CI job is cleaner than carrying it indefinitely. If CI-side perf-regression detection is wanted later, the small 3-case BAliBASE subset in tests/data/ (BB11001, BB12006, BB30014) is the right starting point for a smoke benchmark. --- .github/workflows/benchmark.yml | 136 -------------------------------- 1 file changed, 136 deletions(-) delete mode 100644 .github/workflows/benchmark.yml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index 5dbecd5..0000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: Benchmark - -on: - push: - branches: [main] - paths: - - 'lib/**' - - 'python-kalign/**' - - 'benchmarks/**' - - 'CMakeLists.txt' - - '.github/workflows/benchmark.yml' - pull_request: - branches: [main] - paths: - - 'lib/**' - - 'python-kalign/**' - - 'benchmarks/**' - - 'CMakeLists.txt' - - '.github/workflows/benchmark.yml' - workflow_dispatch: - -permissions: - contents: write - deployments: write - -jobs: - benchmark: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y cmake - - - name: Build C binary - run: | - mkdir -p build - cd build - cmake .. - make -j$(nproc) - - - name: Install Python package - run: | - python -m pip install --upgrade pip - python -m pip install -e . - - - name: Cache BAliBASE dataset - uses: actions/cache@v4 - with: - path: benchmarks/data/downloads - key: benchmark-datasets-balibase-v1 - - - name: Run benchmarks - run: | - python -m benchmarks \ - --dataset balibase \ - --method python_api cli \ - --binary build/src/kalign \ - --output benchmarks/results/latest.json \ - -v - - - name: Check if results were produced - id: check_results - run: | - if [ -f benchmarks/results/latest.json ]; then - echo "has_results=true" >> "$GITHUB_OUTPUT" - else - echo "::warning::No benchmark results produced (dataset download may have failed)" - echo "has_results=false" >> "$GITHUB_OUTPUT" - fi - - - name: Convert results for github-action-benchmark - if: steps.check_results.outputs.has_results == 'true' - run: | - python -c " - import json - with open('benchmarks/results/latest.json') as f: - data = json.load(f) - entries = [] - for method, stats in data.get('summary', {}).items(): - entries.append({ - 'name': f'SP Score Mean ({method})', - 'unit': 'score', - 'value': round(stats['sp_mean'], 2), - 'range': f\"{stats['sp_min']:.1f}-{stats['sp_max']:.1f}\", - }) - entries.append({ - 'name': f'Total Time ({method})', - 'unit': 'seconds', - 'value': round(stats['total_time'], 2), - }) - with open('benchmarks/results/benchmark_output.json', 'w') as f: - json.dump(entries, f, indent=2) - " - - - name: Store benchmark result - if: github.ref == 'refs/heads/main' && steps.check_results.outputs.has_results == 'true' - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: 'customBiggerIsBetter' - output-file-path: benchmarks/results/benchmark_output.json - github-token: ${{ secrets.GITHUB_TOKEN }} - gh-pages-branch: gh-pages - benchmark-data-dir-path: dev/bench - auto-push: true - alert-threshold: '95%' - comment-on-alert: true - fail-on-alert: false - - - name: Compare with baseline (PRs only) - if: github.event_name == 'pull_request' && steps.check_results.outputs.has_results == 'true' - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: 'customBiggerIsBetter' - output-file-path: benchmarks/results/benchmark_output.json - github-token: ${{ secrets.GITHUB_TOKEN }} - gh-pages-branch: gh-pages - benchmark-data-dir-path: dev/bench - auto-push: false - alert-threshold: '95%' - comment-on-alert: true - fail-on-alert: true - - - name: Upload results artifact - if: steps.check_results.outputs.has_results == 'true' - uses: actions/upload-artifact@v4 - with: - name: benchmark-results - path: benchmarks/results/ From f5baaf3b3bb684f5612a9f7c0dc63004f3b83ed3 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 10:14:05 +0800 Subject: [PATCH 26/29] Fix DSSIM matrix mismatch; harden finalise + comparison error paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DSSIM stress test was failing under ASAN on Linux. Root cause: the test built a kalign_run_config via kalign_run_config_defaults() (which returns a protein-oriented config with matrix = PFASUM43) and fed DNA sequences to it. The library correctly refused with "Detected DNA sequences but a protein matrix was selected" and returned FAIL. The test ignored the FAIL return and proceeded into kalign_msa_compare with two un-finalized MSAs, where in turn sort_msa_for_comparison ran with alnlen falling back to seq[0]->len and read past the original (tight-packed) seq->seq buffer that dssim_get_fasta had allocated, triggering the heap-buffer-overflow. Production (CLI + Python bindings) is unaffected because both go through kalign_get_mode_preset(...), which picks a biotype-appropriate matrix automatically. This is a test-side bug that exposed two latent issues in the library error-handling. Fix #1 (root cause): tests/dssim_test.c sets cfg.matrix = KALIGN_MATRIX_AUTO so the library resolves the matrix per biotype, and wraps kalign_align_full / kalign_msa_compare in RUN() so any future alignment failure aborts the test instead of being silently ignored. Fix #2 (defence-in-depth): lib/src/msa_cmp.c — wrap the finalise_alignment(r/t) calls inside kalign_msa_compare, kalign_msa_compare_detailed, and kalign_msa_compare_with_mask in RUN() so a finalise failure propagates as a clean error instead of silently passing a half-finalized MSA into the comparator. Fix #3 (latent library bug): lib/src/msa_op.c — make finalise_alignment atomic w.r.t. msa->sequences. Previously the per-sequence loop replaced seq->seq pointers one at a time; if make_linear_sequence failed on seq[k] the loop bailed leaving seq[0..k-1] swapped to the new buffer and seq[k..numseq-1] still pointing at the original (smaller) buffer — a structurally inconsistent state that any subsequent code reading the MSA would trip over. Now two-pass: build and validate every linear_seq first, then swap pointers only after the whole batch is verified. On error the MSA's buffers are untouched. Fix #4 (API ergonomics): lib/include/kalign/kalign.h — header comment on kalign_run_config_defaults explaining the protein-by-default behaviour and pointing callers at KALIGN_MATRIX_AUTO for biotype auto-selection. Verified: 15/15 ctest pass on macOS clang and on Linux GCC under ASAN inside the memcheck container; 171/171 pytest tests/python/ pass. --- lib/include/kalign/kalign.h | 9 +++++++- lib/src/msa_cmp.c | 12 +++++------ lib/src/msa_op.c | 41 ++++++++++++++++++++++++++----------- tests/dssim.c | 1 + tests/dssim_test.c | 10 ++++++--- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/lib/include/kalign/kalign.h b/lib/include/kalign/kalign.h index 32021a5..ecb97fe 100644 --- a/lib/include/kalign/kalign.h +++ b/lib/include/kalign/kalign.h @@ -104,7 +104,14 @@ EXTERN int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, int *scored_cols, int n_cols, struct poar_score *out); -/* Unified alignment entry point — all callers should use this. */ +/* Unified alignment entry point — all callers should use this. + * + * NOTE: kalign_run_config_defaults() returns a *protein-oriented* config + * (matrix = KALIGN_MATRIX_PFASUM43, protein gap penalties). Callers that + * may operate on DNA or RNA input should set cfg.matrix = KALIGN_MATRIX_AUTO + * before passing the config to kalign_align_full; the library will then + * resolve to a biotype-appropriate matrix at alignment time. The mode-preset + * path (kalign_get_mode_preset) populates the matrix per biotype automatically. */ EXTERN struct kalign_run_config kalign_run_config_defaults(void); EXTERN struct kalign_ensemble_config kalign_ensemble_config_defaults(void); diff --git a/lib/src/msa_cmp.c b/lib/src/msa_cmp.c index 9ff6558..73e72a8 100644 --- a/lib/src/msa_cmp.c +++ b/lib/src/msa_cmp.c @@ -133,11 +133,11 @@ int kalign_msa_compare(struct msa *r, struct msa *t, float *score) if(r->aligned == ALN_STATUS_ALIGNED){ - finalise_alignment(r); + RUN(finalise_alignment(r)); } if(t->aligned == ALN_STATUS_ALIGNED){ - finalise_alignment(t); + RUN(finalise_alignment(t)); } if(r->alnlen == 0 && r->numseq > 0){ @@ -508,10 +508,10 @@ int kalign_msa_compare_detailed(struct msa *r, struct msa *t, ASSERT(out != NULL, "No output struct"); if(r->aligned == ALN_STATUS_ALIGNED){ - finalise_alignment(r); + RUN(finalise_alignment(r)); } if(t->aligned == ALN_STATUS_ALIGNED){ - finalise_alignment(t); + RUN(finalise_alignment(t)); } /* Handle references read from file that had no gaps: @@ -567,10 +567,10 @@ int kalign_msa_compare_with_mask(struct msa *r, struct msa *t, ASSERT(out != NULL, "No output struct"); if(r->aligned == ALN_STATUS_ALIGNED){ - finalise_alignment(r); + RUN(finalise_alignment(r)); } if(t->aligned == ALN_STATUS_ALIGNED){ - finalise_alignment(t); + RUN(finalise_alignment(t)); } if(r->alnlen == 0 && r->numseq > 0){ diff --git a/lib/src/msa_op.c b/lib/src/msa_op.c index fb2ec95..aaf4641 100644 --- a/lib/src/msa_op.c +++ b/lib/src/msa_op.c @@ -553,8 +553,7 @@ static int aln_unknown_warning_message_same_len_no_gaps(void) int finalise_alignment(struct msa* msa) { - struct msa_seq* seq = NULL; - char* linear_seq = NULL; + char** linear_seqs = NULL; int aln_len = 0; ASSERT(msa->aligned == ALN_STATUS_ALIGNED, "Sequences are not aligned"); @@ -563,28 +562,46 @@ int finalise_alignment(struct msa* msa) } aln_len += msa->sequences[0]->len; + /* Two-pass to keep finalisation atomic w.r.t. msa->sequences: + build and validate every linear_seq first; only swap pointers + into msa->sequences[i]->seq once we know every sequence is + consistent. On error the MSA's per-sequence buffers are + untouched and callers can safely retry or fall back. */ + MMALLOC(linear_seqs, sizeof(char*) * msa->numseq); + for(int i = 0; i < msa->numseq; i++) linear_seqs[i] = NULL; + for(int i = 0; i < msa->numseq;i++){ int seq_aln_len = 0; - MMALLOC(linear_seq, sizeof(char)* (aln_len+1)); - memset(linear_seq, '-', aln_len); - linear_seq[aln_len] = 0; - seq = msa->sequences[i]; - RUN(make_linear_sequence(seq, linear_seq, &seq_aln_len)); + struct msa_seq* seq = msa->sequences[i]; + + MMALLOC(linear_seqs[i], sizeof(char) * (aln_len + 1)); + memset(linear_seqs[i], '-', aln_len); + linear_seqs[i][aln_len] = 0; + + RUN(make_linear_sequence(seq, linear_seqs[i], &seq_aln_len)); if(seq_aln_len != aln_len){ ERROR_MSG("Alignment length mismatch: seq %d (%s) " "has length %d, expected %d", i, seq->name, seq_aln_len, aln_len); } - MFREE(seq->seq); - seq->seq = linear_seq; - /* seq->len = aln_len; */ - linear_seq = NULL; } + + /* All sequences validated — commit the swap. */ + for(int i = 0; i < msa->numseq; i++){ + MFREE(msa->sequences[i]->seq); + msa->sequences[i]->seq = linear_seqs[i]; + } + MFREE(linear_seqs); msa->alnlen = aln_len; msa->aligned = ALN_STATUS_FINAL; return OK; ERROR: - if(linear_seq) MFREE(linear_seq); + if(linear_seqs){ + for(int i = 0; i < msa->numseq; i++){ + if(linear_seqs[i]) MFREE(linear_seqs[i]); + } + MFREE(linear_seqs); + } return FAIL; } diff --git a/tests/dssim.c b/tests/dssim.c index d1c4eb2..9130932 100644 --- a/tests/dssim.c +++ b/tests/dssim.c @@ -105,6 +105,7 @@ int dssim_get_fasta(struct msa **msa, int n_seq, int n_obs, int dna,int len, int m->run_parallel = 0; m->consistency_table = NULL; m->quiet = 1; + m->poar_consistency = NULL; MMALLOC(m->sequences, sizeof(struct msa_seq*) * m->alloc_numseq); for(int i = 0; i < 128; i++){ diff --git a/tests/dssim_test.c b/tests/dssim_test.c index 82ce3e4..1673a6e 100644 --- a/tests/dssim_test.c +++ b/tests/dssim_test.c @@ -65,11 +65,15 @@ int test_consistency(int num_tests, int numseq,int dna,int seed) msa_shuffle_seq(m2, rng); { struct kalign_run_config cfg = kalign_run_config_defaults(); - kalign_align_full(m, &cfg, 1, NULL, t1); - kalign_align_full(m2, &cfg, 1, NULL, t2); + /* Defaults select a protein matrix; use AUTO so the + library picks the appropriate matrix per biotype + (so this test exercises both protein and DNA). */ + cfg.matrix = KALIGN_MATRIX_AUTO; + RUN(kalign_align_full(m, &cfg, 1, NULL, t1)); + RUN(kalign_align_full(m2, &cfg, 1, NULL, t2)); } - kalign_msa_compare(m, m2, &score); + RUN(kalign_msa_compare(m, m2, &score)); if(score != 100.0f){ LOG_MSG("Testing %d : %d %d %f", i , t1 ,t2,score); kalign_write_msa(m, NULL, "msf"); From 90d3e74355d6b5987a04bf2674169c45008c6f5b Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 11:21:40 +0800 Subject: [PATCH 27/29] =?UTF-8?q?Add=20tests/check-local.sh=20=E2=80=94=20?= =?UTF-8?q?pre-push=20verification=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Packages the local pre-push checks into one command so the working tree can be self-verified before pushing. Phases (~5 min total): 1. zig build Cross-compile sanity across aarch64-macos, aarch64-linux, x86_64-linux-{gnu,musl}. Catches GCC-vs-Clang divergence. 2. cmake + ctest Native macOS Release build + 15 ctests. 3. podman ASAN Ubuntu container with kalign built under ASAN and the full ctest suite. Catches Linux glibc behaviour that Apple's malloc hides. 4. pytest Python bindings + ecosystem integration (~171 tests). Each phase is independent — no short-circuit, so a single failing phase doesn't prevent the others from running and reporting. Skips gracefully if a tool isn't installed (e.g. podman) or its environment isn't ready (machine not running). Usage: tests/check-local.sh # run all four (~5 min) tests/check-local.sh --quick # skip Linux ASAN (~30s) tests/check-local.sh --help Exits 0 only if every non-skipped phase passes. --- tests/check-local.sh | 198 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 tests/check-local.sh diff --git a/tests/check-local.sh b/tests/check-local.sh new file mode 100755 index 0000000..1f281b9 --- /dev/null +++ b/tests/check-local.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# +# tests/check-local.sh — pre-push verification gate for kalign. +# +# Runs four independent test layers; each catches a different class of +# bug. If every required phase passes, the working tree is safe to push. +# +# Usage: +# tests/check-local.sh # run all four phases (~5 min) +# tests/check-local.sh --quick # skip the Linux ASAN container (~30s) +# tests/check-local.sh --help +# +# Phases: +# 1. zig build — cross-compile sanity across aarch64-macos, +# aarch64-linux, x86_64-linux-{gnu,musl}. +# Catches GCC-vs-Clang divergence (e.g. an unused +# variable that is in fact used inside an #ifdef). +# 2. cmake + ctest — native macOS Release build + 15 ctests. +# Algorithmic correctness, ABI, integration. +# 3. podman ASAN — Ubuntu container with kalign built under ASAN +# and the full ctest suite. Catches Linux glibc +# behaviour that Apple's malloc hides (e.g. +# uninitialised MMALLOC fields that read as zero +# on macOS but garbage on Linux). +# 4. pytest — Python bindings, mode presets, ecosystem +# integration. ~171 tests. +# +# Exits 0 only if every non-skipped phase passes. + +set -u +set -o pipefail + +cd "$(dirname "$0")/.." # repo root + +# ---- options ---------------------------------------------------------- +QUICK=0 +for arg in "$@"; do + case "$arg" in + --quick) QUICK=1 ;; + -h|--help) + sed -n '1,30p' "$0" | sed -n 's/^# \{0,1\}//p' + exit 0 + ;; + *) + printf 'Unknown option: %s\n' "$arg" >&2 + printf 'Try: %s --help\n' "$0" >&2 + exit 2 + ;; + esac +done + +# ---- output helpers --------------------------------------------------- +if [ -t 1 ]; then + YELLOW=$'\033[1;33m' + GREEN=$'\033[0;32m' + RED=$'\033[0;31m' + NC=$'\033[0m' +else + YELLOW='' + GREEN='' + RED='' + NC='' +fi + +banner() { + printf '\n%s═══════════════════════════════════════════════════════════════%s\n' "$YELLOW" "$NC" + printf '%s %s%s\n' "$YELLOW" "$1" "$NC" + printf '%s═══════════════════════════════════════════════════════════════%s\n\n' "$YELLOW" "$NC" +} + +# ---- result tracking -------------------------------------------------- +PASS_LIST=() +FAIL_LIST=() +SKIP_LIST=() + +record_pass() { PASS_LIST+=("$1"); } +record_fail() { FAIL_LIST+=("$1"); } +record_skip() { SKIP_LIST+=("$1 — $2"); } + +# ---- Phase 1: zig build ---------------------------------------------- +phase_zig() { + banner "Phase 1/4: zig build (cross-compile sanity)" + if ! command -v zig >/dev/null 2>&1; then + printf '%s⊘ zig not installed — skipping%s\n' "$YELLOW" "$NC" + record_skip "Phase 1 (zig build)" "zig not installed" + return 0 + fi + if zig build; then + record_pass "Phase 1 (zig build)" + return 0 + fi + record_fail "Phase 1 (zig build)" + return 1 +} + +# ---- Phase 2: native CMake + ctest ----------------------------------- +phase_cmake() { + banner "Phase 2/4: native CMake build + ctest" + local ncpu + ncpu="$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)" + mkdir -p build + if ( cd build \ + && cmake .. -DCMAKE_BUILD_TYPE=Release > /dev/null \ + && make -j"$ncpu" \ + && ctest --output-on-failure ); then + record_pass "Phase 2 (cmake + ctest)" + return 0 + fi + record_fail "Phase 2 (cmake + ctest)" + return 1 +} + +# ---- Phase 3: Linux ASAN container ----------------------------------- +phase_memcheck() { + banner "Phase 3/4: Linux ASAN ctest (podman memcheck container)" + if [ "$QUICK" = "1" ]; then + printf '%s⊘ skipped (--quick)%s\n' "$YELLOW" "$NC" + record_skip "Phase 3 (Linux ASAN)" "--quick" + return 0 + fi + if ! command -v podman >/dev/null 2>&1; then + printf '%s⊘ podman not installed — skipping%s\n' "$YELLOW" "$NC" + record_skip "Phase 3 (Linux ASAN)" "podman not installed" + return 0 + fi + if ! podman info >/dev/null 2>&1; then + printf '%s⊘ podman machine not running — try: podman machine start%s\n' "$YELLOW" "$NC" + record_skip "Phase 3 (Linux ASAN)" "podman machine not running" + return 0 + fi + if ! podman build -f Containerfile.memcheck -t kalign-memcheck . ; then + record_fail "Phase 3 (Linux ASAN — container build)" + return 1 + fi + if podman run --rm kalign-memcheck bash -c \ + "cd /kalign/build-asan && ASAN_OPTIONS='detect_leaks=0:halt_on_error=1:abort_on_error=1' ctest --output-on-failure"; then + record_pass "Phase 3 (Linux ASAN)" + return 0 + fi + record_fail "Phase 3 (Linux ASAN)" + return 1 +} + +# ---- Phase 4: pytest ------------------------------------------------- +phase_python() { + banner "Phase 4/4: Python tests (pytest)" + if ! command -v uv >/dev/null 2>&1; then + printf '%s⊘ uv not installed — skipping%s\n' "$YELLOW" "$NC" + record_skip "Phase 4 (pytest)" "uv not installed" + return 0 + fi + if ! uv pip install -e . \ + --config-settings cmake.args="-DUSE_OPENMP=OFF;-DUSE_THREADPOOL=ON" \ + --force-reinstall --no-deps --quiet; then + record_fail "Phase 4 (pytest — install)" + return 1 + fi + if uv run pytest tests/python/ -q --no-header; then + record_pass "Phase 4 (pytest)" + return 0 + fi + record_fail "Phase 4 (pytest)" + return 1 +} + +# ---- run all phases (each independent; no short-circuit) ------------- +phase_zig || true +phase_cmake || true +phase_memcheck || true +phase_python || true + +# ---- summary --------------------------------------------------------- +banner "Summary" +if [ "${#PASS_LIST[@]}" -gt 0 ]; then + for item in "${PASS_LIST[@]}"; do + printf ' %s✓%s %s\n' "$GREEN" "$NC" "$item" + done +fi +if [ "${#SKIP_LIST[@]}" -gt 0 ]; then + for item in "${SKIP_LIST[@]}"; do + printf ' %s⊘%s %s\n' "$YELLOW" "$NC" "$item" + done +fi +if [ "${#FAIL_LIST[@]}" -gt 0 ]; then + for item in "${FAIL_LIST[@]}"; do + printf ' %s✗%s %s\n' "$RED" "$NC" "$item" + done +fi +echo + +n_fail="${#FAIL_LIST[@]}" +if [ "$n_fail" -eq 0 ]; then + printf '%sAll required phases passed. Safe to push.%s\n' "$GREEN" "$NC" + exit 0 +else + printf '%s%d phase(s) failed. Do NOT push.%s\n' "$RED" "$n_fail" "$NC" + exit 1 +fi From fe02b2488112f819fc18ce5db14288dc18db61c2 Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 11:21:40 +0800 Subject: [PATCH 28/29] Apply black formatting to python-kalign/__init__.py Brings the file into compliance with the version of black used by the python.yml CI lint job. Purely mechanical reformatting (line wrapping of multi-argument calls); no behaviour change. Verified: pytest tests/python/ still passes 171/171. --- python-kalign/__init__.py | 58 +++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/python-kalign/__init__.py b/python-kalign/__init__.py index d2b5225..d352a9c 100644 --- a/python-kalign/__init__.py +++ b/python-kalign/__init__.py @@ -313,7 +313,8 @@ def align( # Gap penalty override rule: if any gap penalty is set, use "fast" preset has_gap_override = ( - gap_open is not None or gap_extend is not None + gap_open is not None + or gap_extend is not None or terminal_gap_extend is not None ) if has_gap_override: @@ -323,7 +324,9 @@ def align( confidence_data = None result = _core.align_mode( - sequences, effective_mode, seq_type_int, + sequences, + effective_mode, + seq_type_int, gap_open if gap_open is not None else -1.0, gap_extend if gap_extend is not None else -1.0, terminal_gap_extend if terminal_gap_extend is not None else -1.0, @@ -457,7 +460,8 @@ def align_from_file( raise ValueError("n_threads must be at least 1") has_gap_override = ( - gap_open is not None or gap_extend is not None + gap_open is not None + or gap_extend is not None or terminal_gap_extend is not None ) if has_gap_override: @@ -466,7 +470,9 @@ def align_from_file( effective_mode = _resolve_mode_name(mode) result = _core.align_from_file_mode( - input_file, effective_mode, seq_type_int, + input_file, + effective_mode, + seq_type_int, gap_open if gap_open is not None else -1.0, gap_extend if gap_extend is not None else -1.0, terminal_gap_extend if terminal_gap_extend is not None else -1.0, @@ -477,8 +483,10 @@ def align_from_file( col_conf = list(conf["column_confidence"]) res_conf = [list(row) for row in conf["residue_confidence"]] return AlignedSequences( - names=names, sequences=sequences, - column_confidence=col_conf, residue_confidence=res_conf, + names=names, + sequences=sequences, + column_confidence=col_conf, + residue_confidence=res_conf, ) else: names, sequences = result @@ -512,10 +520,14 @@ def write_alignment( format_lower = format.lower() format_map = { - "fasta": "fasta", "fa": "fasta", - "clustal": "clustal", "aln": "clustal", - "stockholm": "stockholm", "sto": "stockholm", - "phylip": "phylip", "phy": "phylip", + "fasta": "fasta", + "fa": "fasta", + "clustal": "clustal", + "aln": "clustal", + "stockholm": "stockholm", + "sto": "stockholm", + "phylip": "phylip", + "phy": "phylip", } if format_lower not in format_map: raise ValueError( @@ -524,13 +536,16 @@ def write_alignment( mapped_format = format_map[format_lower] from . import io as _io + if mapped_format == "fasta": _io.write_fasta(sequences, output_file, ids=ids) elif mapped_format == "clustal": _io.write_clustal(sequences, output_file, ids=ids) elif mapped_format == "stockholm": _io.write_stockholm( - sequences, output_file, ids=ids, + sequences, + output_file, + ids=ids, column_confidence=column_confidence, residue_confidence=residue_confidence, ) @@ -651,7 +666,8 @@ def align_file_to_file( n_threads = get_num_threads() has_gap_override = ( - gap_open is not None or gap_extend is not None + gap_open is not None + or gap_extend is not None or terminal_gap_extend is not None ) if has_gap_override: @@ -660,7 +676,11 @@ def align_file_to_file( effective_mode = _resolve_mode_name(mode) _core.align_file_to_file_mode( - input_file, output_file, effective_mode, format, n_threads, + input_file, + output_file, + effective_mode, + format, + n_threads, seq_type_int, gap_open if gap_open is not None else -1.0, gap_extend if gap_extend is not None else -1.0, @@ -701,6 +721,7 @@ def mask_alignment( """ if result.column_confidence is None: import warnings + warnings.warn( "No confidence scores available (requires ensemble mode). " "Returning unmasked alignment." @@ -712,12 +733,12 @@ def mask_alignment( for seq in result.sequences: chars = list(seq) for col in range(len(chars)): - if col < len(conf) and conf[col] < threshold and chars[col] != '-': + if col < len(conf) and conf[col] < threshold and chars[col] != "-": if style == "remove": - chars[col] = '-' + chars[col] = "-" else: chars[col] = chars[col].lower() - masked_seqs.append(''.join(chars)) + masked_seqs.append("".join(chars)) return AlignedSequences( names=result.names, @@ -747,6 +768,7 @@ def filter_alignment( """ if result.column_confidence is None: import warnings + warnings.warn( "No confidence scores available (requires ensemble mode). " "Returning unfiltered alignment." @@ -758,7 +780,7 @@ def filter_alignment( filtered_seqs = [] for seq in result.sequences: - filtered_seqs.append(''.join(seq[col] for col in keep)) + filtered_seqs.append("".join(seq[col] for col in keep)) filtered_conf = [conf[col] for col in keep] filtered_res_conf = None @@ -824,7 +846,7 @@ def write_confidence(path: str, result: AlignedSequences) -> None: """ if result.column_confidence is None: raise ValueError("No confidence scores available (requires ensemble mode)") - with open(path, 'w') as f: + with open(path, "w") as f: for val in result.column_confidence: f.write(f"{val:.4f}\n") From 5bd7602c001930490e52bcb74f28527b32e026de Mon Sep 17 00:00:00 2001 From: Timo Lassmann Date: Sat, 16 May 2026 12:54:01 +0800 Subject: [PATCH 29/29] Bump version to 3.5.2 and add ChangeLog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated in all four locations so the build artefacts agree: pyproject.toml (PyPI metadata) CMakeLists.txt (KALIGN_LIBRARY_VERSION_PATCH) build.zig (zig cross-compile package version) ChangeLog (release notes summary) Release theme: this is the first version that ships the four-mode preset system as the stable public interface (fast / default / recall / accurate), with the Chase-Lev threadpool replacing OpenMP as the default parallelism backend and macOS wheels no longer linking libomp.dylib — closing the conda-forge / numpy OpenMP runtime conflict reported on the issue tracker. Verified: kalign --version reports 3.5.2; kalign.__version__ reports 3.5.2; pre-push checklist (tests/check-local.sh --quick) green prior to this commit. --- CMakeLists.txt | 2 +- ChangeLog | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- build.zig | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b04f887..b5653bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ include(GenerateExportHeader) set(KALIGN_LIBRARY_VERSION_MAJOR 3) set(KALIGN_LIBRARY_VERSION_MINOR 5) -set(KALIGN_LIBRARY_VERSION_PATCH 1) +set(KALIGN_LIBRARY_VERSION_PATCH 2) set(KALIGN_LIBRARY_VERSION_STRING ${KALIGN_LIBRARY_VERSION_MAJOR}.${KALIGN_LIBRARY_VERSION_MINOR}.${KALIGN_LIBRARY_VERSION_PATCH}) diff --git a/ChangeLog b/ChangeLog index 1388445..f2a5611 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,57 @@ -2026-02-27 Timo Lassmann +2026-05-16 Timo Lassmann + + * version 3.5.2 - Four-mode preset system, threadpool, hardening + Algorithm and presets + - Four mode presets stable for protein and nucleotide: + fast, default, recall, accurate. Per-mode configurations + derived by NSGA-III multi-objective optimisation on + BAliBASE v4 (protein) and BRAliBASE (RNA). + - Unified nucleotide preset (DNA and RNA share one path). + - Ensemble alignment with POAR consensus and per-column / + per-residue confidence scores. + - New --add mode: append sequences to an existing alignment. + - Sparse consistency-bonus matrix for large MSAs (avoids + quadratic memory in column count). + - Removed deprecated 'precise' mode alias (Python only; + never shipped as a wheel). + + Parallelism + - Chase-Lev work-stealing thread pool is now the default + parallelism backend; OpenMP is optional (USE_OPENMP=ON). + - macOS wheels no longer link libomp.dylib, resolving the + conda-forge / numpy OpenMP runtime conflict. + + Security / robustness + - POAR file parsing now rejects malformed inputs that would + cause integer overflow in pair-count or per-pair entry + count (lib/src/poar.c). Limited threat model (requires + explicit --load-poar) but tightened anyway. + - finalise_alignment is now atomic w.r.t. msa->sequences: + validates all per-sequence linear lengths before swapping + any pointer, so a failure leaves the MSA unchanged. + - kalign_msa_compare wraps finalise_alignment in RUN(); a + failure surfaces cleanly instead of producing a half- + finalised MSA. + - DSSIM stress test now uses KALIGN_MATRIX_AUTO so it + exercises both protein and DNA biotypes correctly. + - Bumped locked dependencies (cryptography, flask, werkzeug, + pillow, pygments, pytest, urllib3, requests, mako, black) + past their CVE fix versions. + + Build / tooling + - build.zig updated for zig 0.16 (four cross-compile targets). + - tests/check-local.sh: one-command pre-push gate that runs + zig build + native ctest + Linux ASAN container + pytest. + - Containerfile.memcheck (Ubuntu + ASAN + Valgrind) for local + Linux memory-bug reproduction. + - Removed paper-side benchmark machinery, optimizer scripts, + and design-rationale PRDs from the public tree (preserved + in the manuscript repository for reproducibility). + - Cleaned up ~550 lines of dead code (coretralign, + bitShiftRight256ymm, unused split() in bisectingKmeans). + - Build is now warning-free on clang and GCC. + - Dropped the broken benchmark CI workflow (BAliBASE download + endpoint has been gone for months). * version 3.5.1 - Bugfix release - Fix memory leak in build_tree_from_pairwise (realign/ensemble) diff --git a/build.zig b/build.zig index 7409923..1092133 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const kalignPackageVersion = "3.5.1"; +const kalignPackageVersion = "3.5.2"; const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .aarch64, .os_tag = .macos }, diff --git a/pyproject.toml b/pyproject.toml index 83487d2..85dff44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "scikit_build_core.build" [project] name = "kalign-python" -version = "3.5.1" +version = "3.5.2" description = "Python wrapper for the Kalign multiple sequence alignment engine" readme = "README-python.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 4f302a5..3b0ba9e 100644 --- a/uv.lock +++ b/uv.lock @@ -2267,7 +2267,7 @@ wheels = [ [[package]] name = "kalign-python" -version = "3.5.1" +version = "3.5.2" source = { editable = "." } dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },