diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..402761f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: tests + +on: + push: + pull_request: + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v10.2.0 + with: + python-version: "3.12" + - run: uv sync + - run: uv pip show nwgrad + - run: uv run pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfcfab8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.venv/ diff --git a/README.md b/README.md index b6305c8..4270eb2 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,25 @@ # DiscrimAlign -DiscrimAlign is a research codebase for discriminatively learning alignment parameters from labelled pairs of biological sequences. The repository contains the core implementation of the method, the simulation experiments used in the manuscript, a stable `uv` environment, and a manuscript-aligned miRNA case study. +DiscrimAlign provides a ready-to-run miRNA sequence-pair inference workflow backed by bundled trained models, plus the research code used to train and evaluate those models. The main user-facing path is: provide a CSV of sequence pairs, choose a bundled miRNA model, and receive probabilities plus human-readable alignments. ## Repository structure ```text -src/ Core DiscrimAlign implementation +src/ Core DiscrimAlign training and inference code +tests/ Unit and integration tests (pytest) +examples/mirna_pairs.csv Ready-to-run inference input example +case_study_for_mirna/ Bundled miRNA trained models and evaluation workflows +benchmarks/ Backend benchmark script +TODO-nwgrad.md Follow-ups to the nwgrad backend Simulation experiments.ipynb Simulation experiments for the manuscript pyproject.toml Project environment managed by uv -case_study_for_mirna/ miRNA case study, trained models, and evaluation instructions ``` ## Requirements -- Python `>=3.10,<3.13` +- Python `>=3.10` - `uv` for environment management +- `nwgrad` 0.5.2 or later, the default alignment backend. `uv sync` installs it as a binary wheel. Building it from source needs a C++20 compiler that provides ``, such as GCC; on macOS use Homebrew GCC (`CC=gcc-16 CXX=g++-16`), since Apple's clang does not provide it. - JupyterLab or VS Code notebook support for running `Simulation experiments.ipynb` The repository uses a single project environment managed by `uv`. This environment includes the scientific Python dependencies, JupyterLab, an IPython kernel for notebooks, and `miRBench` for the miRNA case-study dataset interface. @@ -55,6 +60,94 @@ uv sync All project commands are run through this environment with `uv run`. +## miRNA Inference Quickstart + +Use this workflow when you want predictions from the bundled trained miRNA models without retraining. + +### 1. Inspect the Example Input + +A ready-to-run CSV is provided at: + +```text +examples/mirna_pairs.csv +``` + +It uses this schema: + +```csv +id,sequence_a,sequence_b +example_positive_like,AUGCUA,AUGGUA +example_short,CUGA,CUGU +``` + +Required columns: + +- `sequence_a`: first miRNA/RNA sequence +- `sequence_b`: second miRNA/RNA sequence +- any extra columns, such as `id`, are preserved in the output + +### 2. Run a Bundled Model + +Two trained miRNA model aliases are available: + +- `manakov`: `case_study_for_mirna/trained_models/manakov_best_model.pkl` +- `hejret`: `case_study_for_mirna/trained_models/hejret_best_model.pkl` + +Run inference: + +```bash +uv run python -m src.infer \ + --model manakov \ + --input examples/mirna_pairs.csv \ + --output predictions_manakov.csv +``` + +Or use the Hejret-trained model: + +```bash +uv run python -m src.infer \ + --model hejret \ + --input examples/mirna_pairs.csv \ + --output predictions_hejret.csv +``` + +### 3. Read the Output + +The output CSV includes: + +- `probability`: logistic model probability for the positive class +- `alignment_score`: score assigned by the fitted aligner +- `aligned_sequence_a`, `alignment_marks`, `aligned_sequence_b`: readable alignment +- `operations`: per-position `match`, `mismatch`, or `gap` +- `normalized_sequence_a`, `normalized_sequence_b`: sequences actually scored by the model + +By default, `--normalize auto` converts `U`/`T` to match the trained model alphabet. Use `--normalize none` only if you want to disable this behavior. + +If your second sequence column contains target/gene sequences before reverse-complementing, pass `--reverse-complement-b` so the second sequence is reverse-complemented before normalization and scoring: + +```bash +uv run python -m src.infer \ + --model manakov \ + --input my_pairs.csv \ + --output my_predictions.csv \ + --seq-a-column noncodingRNA \ + --seq-b-column gene \ + --reverse-complement-b +``` + +### 4. Use Your Own CSV Columns + +If your input columns have different names, pass them explicitly: + +```bash +uv run python -m src.infer \ + --model manakov \ + --input my_pairs.csv \ + --output my_predictions.csv \ + --seq-a-column mirna \ + --seq-b-column target +``` + ## Simulation experiments The notebook @@ -75,12 +168,13 @@ uv run jupyter lab "Simulation experiments.ipynb" With the Python and Jupyter extensions installed, open `Simulation experiments.ipynb` and select the kernel associated with the local `.venv/` environment. -## Core DiscrimAlign usage +## Training API -The main function is `discrimalign` from `src.discrimalign`. +Use `discrimalign` directly when you want to fit a new model instead of using the bundled miRNA models. ```python from src.discrimalign import discrimalign +from src.optimization import create_powerstep seqlistA = ["AUGCUA", "CUGA"] seqlistB = ["AUGGUA", "CUGU"] @@ -93,16 +187,116 @@ result = discrimalign( aligner_mode="local", gap_mode="affine", substitution_mode="symmetric", - num_threads=1, + stepfunction=create_powerstep(1e-5), ) print(result["final_loglik"]) print(result["alpha"]) ``` -The returned object contains the fitted aligner, learned alignment parameters, intercept, final log-likelihood, and optimization trajectories. +The returned object contains the fitted aligner, learned alignment parameters, intercept, alphabet, final log-likelihood, and optimization trajectories. +If `stepfunction` is omitted, `discrimalign` uses a conservative default power step with scale `1e-4`. + +For inference on new sequence pairs, use `predict_pairs` with the fitted result: + +```python +from src import predict_pairs + +rows = predict_pairs( + seqlistA=["AUGCUA"], + seqlistB=["AUGGUA"], + model=result, +) + +print(rows[0]["probability"]) +print(rows[0]["aligned_sequence_a"]) +print(rows[0]["alignment_marks"]) +print(rows[0]["aligned_sequence_b"]) +``` + +Each inference row is a plain dictionary with the input sequences, normalized sequences, alignment score, logistic probability, aligned strings, match markers, and per-position operations (`match`, `mismatch`, or `gap`). By default, inference normalizes `U`/`T` automatically to match the fitted model alphabet; pass `--normalize none` in the CLI to disable this. + +To persist a fitted model and run CSV inference later: + +```python +from src import save_model + +save_model(result, "model.pkl") +``` + +Run inference with a custom saved model: + +```bash +uv run python -m src.infer --model model.pkl --input examples/mirna_pairs.csv --output predictions.csv +``` + +`stepfunction` maps the iteration number to a step size; it defaults to `create_powerstep(1e-4)`. `src.optimization` provides `create_powerstep` and `create_constant_step`. + +`num_threads=0`, the default, chooses the thread count automatically: all logical cores with the nwgrad backend, and one thread with the Biopython backend, whose threads contend for Python's global interpreter lock and only slow it down. Any other value is used as given. For long sequences such as full-length proteins, the number of physical cores can be faster than all logical cores; pass it explicitly. + +`nwgrad_fill` selects nwgrad's vectorized DP fill: `"striped"` (default), `"rowwise"` or `"interpair"`. All three give the same scores, gradients and fit, bit for bit; only the speed differs. On short pairs such as miRNA-target sites the default is the slowest: on all 2.5 million Manakov training pairs (local/affine/general, 300 iterations, 12 threads on an i5-12500), the fit took 1072 s with `"striped"`, 469 s with `"rowwise"` and 291 s with `"interpair"`, which aligns several pairs at once, one per vector lane. On long sequences such as proteins, keep the default. + +### Intercept fit + +At every iteration the intercept α is refitted to the current alignment scores. `alpha_solver` selects how: + +- `"safeguarded_newton"` (default): Newton's method on dL/dα, which is strictly decreasing in α, so its root is the unique optimum. Plain Newton steps are used while they are small, as they are when α changes little between iterations; otherwise the root is bracketed and Newton steps are combined with bisection. The result is exact to rounding from any starting value. +- `"bfgs"`: the previous `scipy.optimize.minimize` fit. When the optimum moves far between iterations, as with `subgradient_scale=1` on large datasets, it can stop far from the optimum. + +On all 2.5 million Manakov training pairs, 300 iterations took 18.4 minutes with the default against 29.6 minutes with `"bfgs"`, and gave the same fit. + +### Backends + +`backend` selects where alignment scores and subgradients come from: + +- `"nwgrad"` (default): the [nwgrad](https://github.com/michalsta/nwgrad) C++ library aligns all pairs in parallel and returns each alignment's score and gradient in one pass. The pairs are encoded once and reused across iterations. +- `"biopython"`: Biopython's `PairwiseAligner`, with the gradient counted in Python from the alignment strings. + +Both backends fit the same model and return the same kinds of objects: the returned `aligner` and `alignments` are Biopython objects with either backend. With `"nwgrad"`, the alignments are nwgrad's own paths at the final parameters, wrapped as `Bio.Align.Alignment` objects, so they are the alignments the final scores and subgradient come from, ties included. Recovering the paths takes one extra alignment pass at the end; pass `return_alignments=False` to skip it. + +The backends agree up to tie-breaking. When two alignments of a pair score exactly the same but use different substitutions or gaps, the backends may pick different ones. Both are valid subgradients, but the fits then drift apart. Exact ties are common with integer-valued scores, such as the default baseline aligner, and with fitted substitution matrices that contain exactly equal entries, such as the zeros a ridge fit assigns to substitutions that never occur in the data. Away from ties, the two backends follow the same trajectory up to floating-point rounding. + +With `"nwgrad"`, the initial estimate is fitted on nwgrad's own alignments of the baseline. A `baseline_aligner` must then be expressible in DiscrimAlign's model: the same gap scores for both sequences and for internal and end gaps, no wildcard, and `local` or `global` mode. Otherwise a `ValueError` explains what is unsupported. + +On the x86-64 machines tested, one fitting iteration on 10,000 miRNA-sized pairs (22 × 50 nt, local alignment, affine gaps, full substitution matrix) was 13–22× faster with nwgrad on one thread than with Biopython, and 77–212× faster with all cores. On 300-residue proteins, where Biopython's C alignment is more competitive, it was 3–5× faster on one thread and 26–108× with all cores. See `benchmarks/` to measure your own machine. + +### Behavior changes + +Compared with earlier versions of DiscrimAlign: + +- Gap scores are kept at or below `-1e-4`, both at the start and after every step. A positive gap score rewards gaps, which makes local alignment ill-posed: Biopython's local mode gives inconsistent answers for it, and the two backends would disagree. +- Subgradient gap counts are fixed for alignments that switch directly between a gap in one sequence and a gap in the other. Each switch now counts as a new gap opening, as Biopython scores it; it was previously counted as an extension. +- In `symmetric` and `general` substitution mode with linear gaps, the initial estimate fits one coefficient on the number of gap columns. It previously added the gap-open and gap-extend coefficients of an affine fit. +- Empty sequences raise a `ValueError` before any alignment work. +- The default backend is nwgrad, and `num_threads` defaults to automatic. +- The intercept α is fitted exactly by a safeguarded Newton method (`alpha_solver="safeguarded_newton"`); the previous BFGS fit is available as `alpha_solver="bfgs"`. +- Labels are checked once, before any alignment work: labels other than 0 and 1, or labels of only one class, raise a `ValueError`, since with one class the likelihood has no finite maximum. This also applies with `initial_parameters`. +- The log-likelihood is computed from the logits, as Σ y·z − Σ log(1 + e^z) with z = α + score, without clipping probabilities. `loglik_trajectory` and `final_loglik` therefore differ from earlier versions on confident predictions, where the clipping capped each pair's loss at about 36. A non-finite score or α raises a `FloatingPointError`. +- `logit_logL` in `src.logit_link` takes `(alignment_scores, alpha, labels)` instead of `(logit_scores, labels)`; calls in the old form raise a `TypeError`. +- The results contain `alphabet`: the alphabet of the fit, whether given, taken from a warm start, or inferred from the sequences. + +## Running tests + +`uv sync` installs `pytest` with the default `dev` dependency group. From the repository root: + +```bash +uv run pytest # full suite, about a minute +uv run pytest -m "not slow" # skip the end-to-end learning runs +``` + +Tests marked `xfail(strict=True)` document known, accepted differences or bugs; they start failing once the behavior changes, and the marker should then be removed. + +## Benchmarks + +`benchmarks/bench_backends.py` times both backends on the current machine: one fitting iteration split into its parts, at several thread counts, plus the initial estimate, for miRNA-sized and protein-sized workloads. + +```bash +uv run python benchmarks/bench_backends.py --quick # a few minutes +uv run python benchmarks/bench_backends.py # full size +uv run python benchmarks/bench_backends.py --fingerprint # hashes to compare machines +``` -Parallel alignment during fitting is chunked when `num_threads > 1`. Each joblib task processes a chunk of sequence pairs rather than a single pair, which reduces scheduler overhead across repeated optimization iterations while preserving alignment order. Thread-based joblib workers are used for the chunked alignment tasks. +The inputs are generated with Python's own random number generator, which gives the same numbers on every platform, so `--fingerprint` output from different machines can be compared directly. nwgrad's results are meant to be bit-identical across CPUs and instruction sets. ## miRNA case study diff --git a/RNA_simulated_general_scheme.ipynb b/RNA_simulated_general_scheme.ipynb index dbd9037..758c21d 100644 --- a/RNA_simulated_general_scheme.ipynb +++ b/RNA_simulated_general_scheme.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "839ba35c", "metadata": {}, "outputs": [], @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "26fdd6c9", "metadata": {}, "outputs": [], @@ -37,7 +37,17 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, + "id": "00bb9c3e", + "metadata": {}, + "outputs": [], + "source": [ + "from Bio.Align import Alignment" + ] + }, + { + "cell_type": "code", + "execution_count": 5, "id": "f46ca6f8", "metadata": {}, "outputs": [], @@ -57,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 7, "id": "7adc03a3", "metadata": {}, "outputs": [], @@ -67,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 8, "id": "9453f626", "metadata": {}, "outputs": [ @@ -75,8 +85,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Using cached dataset /home/dave/.miRBench/datasets/14501607/AGO2_CLASH_Hejret2023/train/dataset.tsv\n", - "Using cached dataset /home/dave/.miRBench/datasets/14501607/AGO2_CLASH_Hejret2023/test/dataset.tsv\n" + "Using cached dataset /home/mciach/.miRBench/datasets/14501607/AGO2_CLASH_Hejret2023/train/dataset.tsv\n", + "Using cached dataset /home/mciach/.miRBench/datasets/14501607/AGO2_CLASH_Hejret2023/test/dataset.tsv\n" ] } ], @@ -87,15 +97,15 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 9, "id": "7416cbc3", "metadata": {}, "outputs": [], "source": [ "mirlist = hejret_train['noncodingRNA']\n", - "mirlist = [Seq(seq) for seq in mirlist]\n", + "mirlist = [str(Seq(seq)) for seq in mirlist]\n", "genelist = hejret_train['gene']\n", - "genelist = [Seq(seq).reverse_complement() for seq in genelist]" + "genelist = [str(Seq(seq).reverse_complement()) for seq in genelist]" ] }, { @@ -116,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 10, "id": "63449520", "metadata": {}, "outputs": [], @@ -133,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 11, "id": "93e877ba", "metadata": {}, "outputs": [], @@ -147,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 12, "id": "bde6153a", "metadata": {}, "outputs": [], @@ -157,13 +167,13 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 13, "id": "11196cde", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -180,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 25, "id": "5d6bc1e2", "metadata": {}, "outputs": [], @@ -191,13 +201,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 15, "id": "f9a4ccd3", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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jxw5osLCQo0dO1Yej0eXXHIJvztOQ6jr/MILL+jSSy/ViBEjlJGRoYULF+r48eODNNvItGXLFt12221KT09XVFSU3n777a+9zubNm3XllVfK4/Hooosu0htvvDHg8wzLd/GEU0VFhYuPj3evvfaa2717t3vggQdcUlKSa2xs7HX81q1bXUxMjFuxYoXbs2ePW7JkiYuLi3N1dXWDPPPIEuo633vvva68vNzt2rXL7d271913333O5/O5v//974M888gS6jp3O3DggPvGN77hrrvuOnf77bcPzmQjWKjr3Nra6qZOnepuueUW9+GHH7oDBw64zZs3u9ra2kGeeWQJdZ1Xr17tPB6PW716tTtw4IB799133dixY93ChQsHeeaRZcOGDe6JJ55wb731lpPk1q1b9z/H79+/340cOdIVFxe7PXv2uJdeesnFxMS4jRs3Dug8h12gTJs2zRUWFgZ/7uzsdOnp6W758uW9jr/77rtdbm5uj33Tp0933/3udwd0npEu1HU+UUdHh0tMTHRvvvnmQE1xSOjLOnd0dLirr77a/exnP3MFBQUEymkIdZ1ffvlld8EFF7i2trbBmuKQEOo6FxYWuptuuqnHvuLiYnfNNdcM6DyHktMJlEcffdRddtllPfbNnj3b5eTkDODMnBtWp3ja2tpUU1Oj7Ozs4L7o6GhlZ2erurq61+tUV1f3GC9JOTk5pxyPvq3zib766iu1t7crOTl5oKYZ8fq6zkuXLlVKSormzZs3GNOMeH1Z59/97nfy+/0qLCxUamqqLr/8ci1btkydnZ2DNe2I05d1vvrqq1VTUxM8DbR//35t2LBBt9xyy6DMebgI1/NgRHybcX/5/PPP1dnZedKn1aampuqTTz7p9TqBQKDX8YFAYMDmGen6ss4neuyxx5Senn7SPwr8R1/W+cMPP9Srr76q2traQZjh0NCXdd6/f7/ef/99zZkzRxs2bNCnn36qhx56SO3t7SotLR2MaUecvqzzvffeq88//1zXXnutnHPq6OjQgw8+qMcff3wwpjxsnOp5sKWlRf/61780YsSIAbnfYXUEBZGhrKxMFRUVWrdunRISEsI9nSHj6NGjys/P1yuvvKKzzz473NMZ0rq6upSSkqKf/vSnysrK0uzZs/XEE09o1apV4Z7akLJ582YtW7ZMK1eu1Mcff6y33npLlZWVevrpp8M9NfSDYXUE5eyzz1ZMTIwaGxt77G9sbFRaWlqv10lLSwtpPPq2zt2effZZlZWV6fe//70mT548kNOMeKGu81//+ld99tlnuu2224L7urq6JEmxsbHat2+fLrzwwoGddATqy9/nsWPHKi4uTjExMcF9EydOVCAQUFtbm+Lj4wd0zpGoL+v85JNPKj8/X9/5znckSZMmTdKxY8c0f/58PfHEE4qO5v/B+8Opnge9Xu+AHT2RhtkRlPj4eGVlZamqqiq4r6urS1VVVfL7/b1ex+/39xgvSZs2bTrlePRtnSVpxYoVevrpp7Vx40ZNnTp1MKYa0UJd5wkTJqiurk61tbXB7dvf/rZuvPFG1dbWKiMjYzCnHzH68vf5mmuu0aeffhoMQEn685//rLFjxxInp9CXdf7qq69OipDuKHR8zVy/Cdvz4IC+BNegiooK5/F43BtvvOH27Nnj5s+f75KSklwgEHDOOZefn+8WL14cHL9161YXGxvrnn32Wbd3715XWlrK24xPQ6jrXFZW5uLj491vfvMbd+jQoeB29OjRcD2EiBDqOp+Id/GcnlDXub6+3iUmJrqioiK3b98+t379epeSkuKeeeaZcD2EiBDqOpeWlrrExET3y1/+0u3fv9+999577sILL3R33313uB5CRDh69KjbtWuX27Vrl5PknnvuObdr1y73t7/9zTnn3OLFi11+fn5wfPfbjBctWuT27t3rysvLeZvxQHnppZfcuHHjXHx8vJs2bZr76KOPgpddf/31rqCgoMf4X//61+6SSy5x8fHx7rLLLnOVlZWDPOPIFMo6n3feeU7SSVtpaengTzzChPr3+b8RKKcv1HXetm2bmz59uvN4PO6CCy5wP/zhD11HR8cgzzryhLLO7e3t7vvf/7678MILXUJCgsvIyHAPPfSQ++c//zn4E48gf/jDH3r9fdu9tgUFBe76668/6TpTpkxx8fHx7oILLnCvv/76gM8zyjmOgwEAAFuG1WtQAABAZCBQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADm/D9UHjVEa5rdIAAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -214,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 26, "id": "f1f27744", "metadata": {}, "outputs": [ @@ -223,7 +233,7 @@ "output_type": "stream", "text": [ "Sum of log-logit scores: -11654.43085041492\n", - "True LogL: -2968.1308504149256\n" + "True LogL: -3019.9308504149258\n" ] } ], @@ -237,19 +247,19 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 27, "id": "1467d4dd", "metadata": {}, "outputs": [], "source": [ "const_step = create_constant_step(0.00005)\n", "# powerstep = create_powerstep(0.00005, power=0.5, burnin=0)\n", - "powerstep = create_powerstep(0.00002, power=-0.1, burnin=0)" + "# powerstep = create_powerstep(0.00002, power=-0.1, burnin=0)" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 28, "id": "5bd62128", "metadata": {}, "outputs": [], @@ -259,7 +269,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "0470b432", "metadata": { "scrolled": true @@ -271,14 +281,16 @@ " aligner_mode='local',\n", " substitution_mode='general',\n", " gap_mode='affine', \n", - " stochastic_factor=0.01,\n", - " verbose=True, max_iter=NITER,\n", - " num_threads = 24)" + " stochastic_factor=0.001,\n", + " verbose=False, max_iter=NITER,\n", + " backend='nwgrad',\n", + " alpha_solver='bfgs'\n", + " )" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 40, "id": "09ffecd4", "metadata": {}, "outputs": [ @@ -286,7 +298,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-2931.755443027663\n" + "-3013.3755093600403\n" ] } ], @@ -296,13 +308,13 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 41, "id": "61ca6b04", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -340,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "d8511ad7", "metadata": {}, "outputs": [ @@ -348,28 +360,28 @@ "name": "stdout", "output_type": "stream", "text": [ - "-1.2 -1.1851905329697283\n", - "-0.1 -0.10008519066004118\n", - "-12 -12.508575909332592\n", - "A A 1.0 1.0104810615788193\n", - "A C -0.3 -0.22698409597437372\n", - "A T -1.0 -0.8214807581847762\n", - "A G -0.8 -0.6561839939193942\n", - "C A -0.6 -0.48426625859883055\n", - "C C 1.2 1.2172779269001817\n", - "C T -0.3 -0.41717028239192755\n", - "C G -1.0 -0.8998356615237482\n", - "T A -1.2 -1.0434559189566939\n", - "T C -0.4 -0.3578481356855115\n", - "T T 1.0 1.026599951981614\n", - "T G -0.8 -0.7567804457349925\n", - "G A -0.4 -0.4718224804451187\n", - "G C -1.4 -1.056223528410227\n", - "G T -0.9 -0.8720294472245051\n", - "G G 1.3 1.311730936456145\n", - "[[1. 0.99427657]\n", - " [0.99427657 1. ]]\n", - "0.09249964972129707\n" + "-1.2 -1.165343564314946\n", + "-0.1 -0.09757528776836821\n", + "-12 -11.353828161360962\n", + "A A 1.0 0.9210889140264713\n", + "A C -0.3 -0.30243718879655124\n", + "A T -1.0 -0.8730550794186046\n", + "A G -0.8 -0.7375942360772312\n", + "C A -0.6 -0.46340674363922557\n", + "C C 1.2 1.1498021912179925\n", + "C T -0.3 -0.36470128872991037\n", + "C G -1.0 -0.9478253794655838\n", + "T A -1.2 -1.1721660903395053\n", + "T C -0.4 -0.4498243421369788\n", + "T T 1.0 0.9464201331607637\n", + "T G -0.8 -0.6017130265826758\n", + "G A -0.4 -0.43158193978402526\n", + "G C -1.4 -1.1417182448940837\n", + "G T -0.9 -0.9099185451299491\n", + "G G 1.3 1.2312239100252487\n", + "[[1. 0.99593254]\n", + " [0.99593254 1. ]]\n", + "0.07952808473312678\n" ] } ], @@ -393,13 +405,13 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 27, "id": "3d3e3dd0", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -417,13 +429,13 @@ "plt.ylabel(\"DiscrimAlign estimated weight\")\n", "\n", "plt.tight_layout()\n", - "ax = plt.gca()\n", - "xmin, xmax = ax.get_xlim()\n", - "ax.set_xticks(np.linspace(-2, 1, 4))\n", - "ax.set_xlim(xmin, xmax)\n", + "#ax = plt.gca()\n", + "#xmin, xmax = ax.get_xlim()\n", + "#ax.set_xticks(np.linspace(-2, 1, 4))\n", + "#ax.set_xlim(xmin, xmax)\n", "\n", - "ax.set_yticks(np.linspace(*ax.get_ylim(), 4, dtype=int))\n", - "plt.savefig(\"results/general/true_vs_estimated_submatrix.png\", dpi=DPI)" + "#ax.set_yticks(np.linspace(*ax.get_ylim(), 4, dtype=int))\n", + "#plt.savefig(\"results/general/true_vs_estimated_submatrix.png\", dpi=DPI)" ] }, { @@ -538,7 +550,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 28, "id": "679bddd1", "metadata": {}, "outputs": [], @@ -548,7 +560,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 29, "id": "87f81972", "metadata": {}, "outputs": [], @@ -558,7 +570,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 45, "id": "6c368df8", "metadata": {}, "outputs": [ @@ -569,7 +581,7 @@ " 4.0e-05, 4.5e-05, 5.0e-05])" ] }, - "execution_count": 51, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } @@ -581,7 +593,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 31, "id": "56d860f9", "metadata": {}, "outputs": [], @@ -591,7 +603,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 46, "id": "b30b3d05", "metadata": {}, "outputs": [], @@ -606,7 +618,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 47, "id": "0e1d8682", "metadata": {}, "outputs": [], @@ -618,10 +630,27 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 48, "id": "ce02d4d1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step 5.00e-06 did NOT hit logL\n", + "step 1.00e-05 did NOT hit logL\n", + "step 1.50e-05 did NOT hit logL\n", + "step 2.00e-05 did NOT hit logL\n", + "step 2.50e-05 did NOT hit logL\n", + "step 3.00e-05, surpassed logL at iteration 189 and reached a maximum value of -2912.562703887873\n", + "step 3.50e-05, surpassed logL at iteration 163 and reached a maximum value of -2911.2999156611527\n", + "step 4.00e-05, surpassed logL at iteration 143 and reached a maximum value of -2910.270122432674\n", + "step 4.50e-05, surpassed logL at iteration 127 and reached a maximum value of -2909.679296617294\n", + "step 5.00e-05, surpassed logL at iteration 126 and reached a maximum value of -2909.8666768056964\n" + ] + } + ], "source": [ "pickle_path = \"results/general/step_experiment_general.pkl\"\n", "\n", @@ -669,30 +698,30 @@ " else:\n", " print(f\"step {stepl:.2e} did NOT hit logL\")\n", " \n", - " with open(pickle_path, \"wb\") as f:\n", - " pickle.dump(\n", - " {\n", - " \"labels\": labels,\n", - " \"true_logL\": true_logL,\n", - " \"discrimalign_results_step\": discrimalign_results_step,\n", - " \"logL_reached_iters\": logL_reached_iters,\n", - " \"final_vals\": final_vals,\n", - " \"steplengths\": steplengths,\n", - " \"NITER\": NITER,\n", - " },\n", - " f\n", - " )" + "# with open(pickle_path, \"wb\") as f:\n", + "# pickle.dump(\n", + "# {\n", + "# \"labels\": labels,\n", + "# \"true_logL\": true_logL,\n", + "# \"discrimalign_results_step\": discrimalign_results_step,\n", + "# \"logL_reached_iters\": logL_reached_iters,\n", + "# \"final_vals\": final_vals,\n", + "# \"steplengths\": steplengths,\n", + "# \"NITER\": NITER,\n", + "# },\n", + "# f\n", + "# )" ] }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 49, "id": "44dad79d", "metadata": {}, "outputs": [ { "data": { - "image/png": "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7C7Kz33uczz88MNs27aNbdu28fDDD/P5z3++tCnzsmXLmDJlyl4977XXXsu0adMIBAKUlZWxaNEili5d2q9MLBbj/PPPJxwOE41G+fznP086ne5XRgjBzTffzJQpU/B4PDQ0NHDdddf1K3P77bczffp0fD4fU6dO5f77799t3bq7uznllFOor6/H4/HQ2NjI5ZdfXgp0gOeffx5FUXa5tLW17dX7sE+EWhqDGyzH5CxKSZJGg2FNMvnFL37BFVdcwTnnnINlWcUn0nUuuugibrnlFgCmTZvG3XffvVfPO2XKFG677TYmTJhALpfjlltu4aSTTmL9+vWlk62ef/75tLa28tRTT2GaJp/97Ge59NJL+c1vflN6nq9//ev8/e9/5+abb2bWrFnEYjFisVjp8TvuuIMlS5bwy1/+kiOOOIJly5ZxySWXUFZWxr/9278NWDdVVTnjjDP4n//5H6qqqli/fj2XXXYZsVis3/cGWLNmTb/u2+rq6r16H/aFIugbgxvk/xfZgpMkaTRQxD7stZVOp9m4cSMAEyZMIBgM7reKASSTSSKRCE8//TQnnHAC7733HjNmzOC1115j3rx5ADz55JOcdtppbNu2jfr6et577z1mz57NO++8U5rh+X4LFy7k6KOP5qabbiodu+qqq1i6dCkvvfTSkOv305/+lJtuuomtW7cCxRbc8ccfT09PD9FodJ9fdyKR6BeUQ/HbL32bxCybP02cSyBURyg0DgCXomD2/qhvn9FEWJdnhZMk6aNnbz4f92knk2AwSHl5OeXl5fs93AzD4K677iISiTBnzhwAXn31VaLRaCncABYtWoSqqqWuzEcffZQJEybw2GOPMX78eMaNG8cXvvCFfi24QqGA1+vt9/18Ph/Lli3DNM0h1a+lpYWHHnqIY489dpfH5s6dS11dHSeeeCIvv/zyHp+rUCiQTCb7XYatXwuur6Xm2alfUs6ilCRpNBhWwDmOw3/9138RiURoamqiqamJaDTKf//3f+/zqXMee+wxgsEgXq+XW265haeeeorKykoA2tradunu03Wd8vLy0jjXxo0b2bJlC3/4wx+4//77+fWvf83y5cv51Kc+Vfqak08+mbvvvpvly5cjhOD111/n7rvvxjRNurq6dlu/c889F7/fT0NDA+FwuF83bF1dHXfeeSd/+tOf+NOf/kRjYyPHHXcc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", + "image/png": "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18JJB46Vz+KoWr9BJrsPMDmRiSRJY5WcinCMsXsCbrg8sgUnSdIYtc8B9//+3/+juLh4NOoijQLHNBH68FpvIAeZSJI0dg17FOVQbr755tGohzRKbCuH0IbfgpMBJ0nSWDWigLvpppt2+byiKPh8PqZMmcK5554rW3YHgG3mcbTeeSj3HF7yOjhJksaqEQXc8uXLWbZsGbZtc+ihhwKwbt06NE1j+vTp/OxnP+NrX/sar776KjNmzBjVCku755h5hLr7LkqvqpBz3MFBchSlJElj1YjOwZ177rksWLCAhoYGli5dytKlS9mxYwennXYal1xyCTt37uTjH/84N95442jXV9qD4QScT+37tcsuSkmSxqoRBdydd97Jf/zHfxCJRArPRaNRbrnlFu644w4CgQDf+973WLp06ahVVBoeYVsIbfe/1v4BJ1twkiSNVSMKuHg8TktLy6DnW1tbSSQSAMRiMfL5/L7VTtprjpXH6R1jMkR4+bS+5zWZb5IkjVEj7qK86qqreOyxx9ixYwc7duzgscce4+qrry5MyrxkyRKmTZu2V597yy23MH36dILBIEVFRSxYsIA333xzQJmOjg4uu+wyIpEIsViMq6++mmQyOaDMu+++y4knnojP56O2tpY77rhjwOsrV67k/PPPp76+HkVRuPvuu/dYt2w2yxVXXMHMmTPRdX2Xk09fccUVKIoyaDv88MP3aj/sC8c0C1dvDzXIxCu7KCVJGgdGFHA///nPOfXUU7n44oupq6ujrq6Oiy++mFNPPZX7778fgOnTp/PLX/5yrz532rRp3Hvvvbz33nu8+uqr1NfXc/rpp9Pa2looc9lll7Fy5UoWLVrEU089xcsvv8y1115beD2RSHD66adTV1fH0qVLufPOO7nlllv4xS9+USiTTqeZPHkyt99+O5WVlcOqm23b+P1+rr/+ehYsWLDLMvfccw+NjY2Fbfv27RQXF/PZz352r/bDvnBss2ceSga04Pp3RfrUXT8vSZI0lihiH+baSiaTbNq0CYDJkycTCoVGrWLghlU0GmXx4sWceuqprF69mhkzZvDWW29x9NFHA/DMM89w9tlns2PHDqqrq7nvvvv4zne+Q1NTE4ZhAPDtb3+bxx9/nDVr1gz6jvr6em644QZuuOGGYdfriiuuoKuri8cff3y35R5//HE+85nPsHnz5r1aJLb3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1ltvFcr87W9/E4qiiJ07dwohhNi8ebMAxPLly4f1HXV1deK///u/96pen//858W555476Pk777xTTJ48ecBzP/7xj0VNTc1efX7vzx2Px/fqfZIkSWPd3hwfD6oWXH/5fJ5f/OIXRKNRZs+eDcAbb7xBLBbj6KOPLpRbsGABqqoO6spcuHAh5eXlnHDCCTzxxBMfSp3nz5/P9u3befrppxFC0NzczJ/+9CfOPvvsD+X7JUmSpD4H3XVwTz31FBdffDHpdJqqqioWLVpEaWkpAE1NTZSXlw8or+s6xcXFNDU1ARAKhfjRj37E8ccfj6qq/PnPf+a8887j8ccfZ+HChfu17scffzyPPPIIF110EdlsFsuy+NSnPsVPf/rT3b4vl8uRy+UKj+PxOACJRGK/1leSJOmjpve4KITYc+H93p4cwv/+7/+KYDBY2F5++WUhhBDJZFKsX79evPHGG+Kqq64S9fX1orm5WQghxK233iqmTZs26LPKysrEz372syG/6/LLLxcnnHDCLl8bzS7KlStXiqqqKnHHHXeId955RzzzzDNi5syZ4qqrrtrt533/+98XgNzkJje5yW2Y2/bt2/d4rD5gLbiFCxcyb968wuOamhoAgsEgU6ZMYcqUKXzsYx9j6tSp/OpXv+Lmm2+msrKSlpaWAZ9jWRYdHR27HSwyb948Fi1atH9+kH5uu+02jj/+eL7xjW8AMGvWLILBICeeeCL/+Z//SVVV1S7fd/PNN3PTTTcVHjuOQ0dHByUlJXs1n2QikaC2tpbt27cTiUT27YcZ4+S+Gj65r4ZH7qfh25d9JYSgu7ub6urqPZY9YAEXDocJh8N7LOc4TqH7bv78+XR1dbF06dLCqMjnn38ex3EGhOUHrVixYshwGU3pdBpdH7hLNc2d3V/spjnt9Xrxer0DnovFYiOuRyQSkf/Bhknuq+GT+2p45H4avpHuq2g0OqxyB805uFQqxa233srChQupqqqira2Nn/70p+zcuZPPfvazABx22GGceeaZXHPNNdx///2YpslXvvIVLr744kKaP/TQQxiGwZFHHgnAo48+ygMPPMAvf/nLwnfl83lWrVpVuL9z505WrFhBKBRiypQpANx777089thjPPfcc4X3rVq1inw+T0dHB93d3axYsQKAOXPmAPCpT32Ka665hvvuu48zzjiDxsZGbrjhBo49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", "text/plain": [ "
" ] @@ -717,7 +746,7 @@ "ax.yaxis.set_major_formatter(FormatStrFormatter('%.2f'))\n", "\n", "plt.tight_layout()\n", - "plt.savefig(\"results/general/step_experiment_full_trajectories_general.png\", dpi=DPI)" + "# plt.savefig(\"results/general/step_experiment_full_trajectories_general.png\", dpi=DPI)" ] }, { diff --git a/TODO-nwgrad.md b/TODO-nwgrad.md new file mode 100644 index 0000000..739bf4d --- /dev/null +++ b/TODO-nwgrad.md @@ -0,0 +1,63 @@ +# TODO: follow-ups to the nwgrad backend + +Items deliberately left out of the nwgrad backend work, roughly in the order +they become relevant. + +## Validation + +- **Real data:** run both backends on the miRNA case study and compare the + fitted models. So far, equivalence is established on simulated data only: + DNA and protein, up to 400 residues, at most 80 pairs per fit in the test + suite. +- **Notebooks:** rerun the simulation notebooks under the new defaults + (`backend="nwgrad"`, gap cap, linear-gap estimator fix). Their results will + shift slightly. + +## Known issues in DiscrimAlign + +- **scikit-learn 1.10** removes `LogisticRegression(penalty=None)`, used by the + simple-mode initial estimators in `src/optimization.py`. They will stop + working; the replacement is `C=np.inf`. It is deprecated, with a warning, + since 1.8. +- **`src.discrimalign` is shadowed:** `src/__init__.py` imports the function + under the module's name, so `import src.discrimalign` binds the function. + Use `sys.modules["src.discrimalign"]` to reach the module. +- **`tol` is accepted but unused** (TODO in `discrimalign()`). + +## The gap-score cap + +`discrimalign()` keeps gap scores at or below `_MAX_GAP_SCORE = -1e-4` +(`src/discrimalign.py`). The cap should be 0. It is not, because at a gap score +of exactly 0, zero-cost gap columns tie with no gap: the backends return equal +scores but different gap counts, i.e. different valid subgradients, and the +fits diverge. Reset the cap to 0 once that kink is handled consistently, e.g. +by making both backends break such ties the same way. + +## nwgrad-side follow-ups + +- **Local mode with positive gap scores.** nwgrad lets a local alignment end + with a gap column but not start with one. That only differs from the + alternatives when a gap column scores positive, which the cap prevents. + Biopython's local mode is itself inconsistent there, so there is no reference + to match. One strict xfail in `tests/test_nwgrad_backend.py` documents it. A + clean definition would require local alignments to start and end with an + aligned column, in every DP fill. +- **Default thread count.** nwgrad's `n_threads=0` picks physical cores, which + is about 15–25% slower than all logical cores for short pairs, where the DP + tables stay in cache. See `TODO.md` in nwgrad. DiscrimAlign therefore resolves + `num_threads=0` to `os.cpu_count()` itself; long-sequence (protein) workloads + may do better with physical cores (TODO in `discrimalign()`). +- **Per-pair gradients in bulk.** nwgrad `main` has `SeqPairBatch.grads()` + (`c159946`), which returns all per-pair gradients as two arrays. The + initial estimate uses it when the installed nwgrad has it, and otherwise + falls back to reading `batch[i].grad.to_dict()` pair by pair (about 5x + slower). At the next nwgrad release, require it and remove + `NwgradEngine._count_arrays_per_pair()`. +- **float32.** The backend uses nwgrad's double-precision classes with pointer + traceback. The float32 classes would be faster, but nwgrad's default float32 + traceback can return a suboptimal path, and Biopython works in double. + +## Pending upstream work + +- **PR #10** (`theta_trajectory`) edits the optimization loop this work + refactored. It needs to be ported onto the shared loop in `discrimalign()`. diff --git a/benchmarks/bench_backends.py b/benchmarks/bench_backends.py new file mode 100644 index 0000000..1402516 --- /dev/null +++ b/benchmarks/bench_backends.py @@ -0,0 +1,340 @@ +""" +Benchmark discrimalign's backends on this machine. + +Run from the repository root: + + python benchmarks/bench_backends.py # all workloads + python benchmarks/bench_backends.py --quick # smaller and faster + python benchmarks/bench_backends.py --fingerprint # hashes for cross-machine checks + +For each workload it times one subgradient iteration, split into the alignment +(and scores), the intercept fit, the gradient and the rest, for the nwgrad +backend at several thread counts and for the Biopython backend on one thread +(its threads contend for the GIL). It also times the initial estimate. +Results are printed as JSON lines (or written with --json) and summarized in a +table at the end. + +All inputs are generated with Python's own random module, which produces the +same numbers on every platform, so --fingerprint hashes from different +machines can be compared directly: nwgrad's results are meant to be +bit-identical across CPUs and instruction sets. +""" +import argparse +import hashlib +import json +import os +import platform +import random +import statistics +import sys +import time +import warnings + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import numpy as np +from Bio.Align import PairwiseAligner, substitution_matrices +from scipy.optimize import minimize + +import nwgrad +from src.discrimalign import _BiopythonEngine, _align_pairs, discrimalign +from src.logit_link import logit_logL, logit_partial_scores +from src.nwgrad_backend import NwgradEngine, baseline_parameters +from src.optimization import (create_powerstep, get_initial_estimate, + get_initial_estimate_from_counts) + +DNA = "ACGT" +PROTEIN = "ACDEFGHIKLMNPQRSTVWY" + +# name: (alphabet, mode, gap model, substitution mode, pair maker, default pair count) +WORKLOADS = { + "mirna-local": (DNA, "local", "affine", "general", "mirna", 10000), + "mirna-global": (DNA, "global", "linear", "simple", "mirna", 10000), + "protein": (PROTEIN, "local", "affine", "general", "protein", 2000), +} + + +# --- portable inputs ---------------------------------------------------------- + +def random_seq(rng, length, alphabet): + return "".join(rng.choice(alphabet) for _ in range(length)) + + +def mutate(rng, seq, alphabet, sub_rate, indel_rate): + out = [] + for char in seq: + u = rng.random() + if u < indel_rate / 2: + continue + if u < indel_rate: + out.append(rng.choice(alphabet)) + if rng.random() < sub_rate: + char = rng.choice([c for c in alphabet if c != char]) + out.append(char) + return "".join(out) or seq[0] + + +def mirna_pairs(rng, n): + """22-nt queries against 50-nt targets; every other target hides a mutated copy.""" + A, B, y = [], [], [] + for k in range(n): + query, target = random_seq(rng, 22, DNA), random_seq(rng, 50, DNA) + if k % 2 == 0: + site = mutate(rng, query, DNA, 0.15, 0.05) + pos = rng.randrange(0, 50 - len(site)) + target = target[:pos] + site + target[pos + len(site):] + A.append(query) + B.append(target[:50]) + y.append(1 - k % 2) + return A, B, np.array(y) + + +def protein_pairs(rng, n, length=300): + """Mutated homologs and unrelated pairs of 300 residues.""" + A, B, y = [], [], [] + for k in range(n): + seq = random_seq(rng, length, PROTEIN) + A.append(seq) + B.append(mutate(rng, seq, PROTEIN, 0.3, 0.05) if k % 2 == 0 else random_seq(rng, length, PROTEIN)) + y.append(1 - k % 2) + return A, B, np.array(y) + + +def random_params(rng, gap_mode, substitution_mode, alphabet): + """Non-integer parameters, so alignments with different counts do not tie.""" + params = {"alpha": rng.uniform(-1.5, -0.5)} + if gap_mode == "affine": + params["open_gap_score"] = rng.uniform(-4.0, -2.5) + params["extend_gap_score"] = rng.uniform(-1.5, -0.3) + else: + params["gap_score"] = rng.uniform(-3.0, -1.0) + if substitution_mode == "simple": + params["match_score"] = rng.uniform(1.5, 3.0) + params["mismatch_score"] = rng.uniform(-2.0, -0.5) + else: + n = len(alphabet) + data = np.array([[rng.uniform(1.5, 3.0) if i == j else rng.uniform(-2.0, -0.5) + for j in range(n)] for i in range(n)]) + if substitution_mode == "symmetric": + data = (data + data.T) / 2 + params["substitution_matrix"] = substitution_matrices.Array(alphabet=alphabet, data=data) + return params + + +def make_workload(name, n_pairs, seed=0): + alphabet, mode, gap_mode, substitution_mode, maker, _ = WORKLOADS[name] + rng = random.Random(seed) + A, B, y = (mirna_pairs if maker == "mirna" else protein_pairs)(rng, n_pairs) + params = random_params(random.Random(seed + 1), gap_mode, substitution_mode, alphabet) + return A, B, y, params, alphabet, mode, gap_mode, substitution_mode + + +def default_baseline(mode, gap_mode, substitution_mode, alphabet): + """discrimalign()'s default baseline aligner.""" + aligner = PairwiseAligner() + aligner.mode = mode + if gap_mode == "affine": + aligner.open_gap_score, aligner.extend_gap_score = -8, -0.5 + else: + aligner.gap_score = -6 + if substitution_mode == "simple": + aligner.match_score, aligner.mismatch_score = 5, -4 + else: + aligner.substitution_matrix = substitution_matrices.Array( + data=9 * np.eye(len(alphabet)) - 4, alphabet=alphabet) + return aligner + + +def make_aligner(mode, params): + aligner = PairwiseAligner() + aligner.mode = mode + if "gap_score" in params: + aligner.gap_score = params["gap_score"] + else: + aligner.open_gap_score = params["open_gap_score"] + aligner.extend_gap_score = params["extend_gap_score"] + if "substitution_matrix" in params: + aligner.substitution_matrix = params["substitution_matrix"] + else: + aligner.match_score = params["match_score"] + aligner.mismatch_score = params["mismatch_score"] + return aligner + + +# --- timing ------------------------------------------------------------------- + +def fit_alpha(scores, labels, alpha0): + target = lambda a: -logit_logL(scores, a[0], labels) + fprime = lambda a: -np.sum(labels - logit_partial_scores(scores, a)) + return minimize(target, alpha0, jac=fprime)["x"][0] + + +def time_iterations(engine, params, labels, reps): + """Median seconds per part of one iteration, after one warm-up iteration.""" + parts = {k: [] for k in ("align", "alpha", "grad", "other")} + for rep in range(reps + 1): + t0 = time.perf_counter() + engine.set_params(params) + scores = engine.scores() + t1 = time.perf_counter() + logit_logL(scores, params["alpha"], labels) + t2 = time.perf_counter() + alpha = fit_alpha(scores, labels, params["alpha"]) + t3 = time.perf_counter() + engine.raw_subgradient(logit_partial_scores(scores, alpha), labels, alpha) + t4 = time.perf_counter() + if rep: + parts["align"].append(t1 - t0) + parts["other"].append(t2 - t1) + parts["alpha"].append(t3 - t2) + parts["grad"].append(t4 - t3) + out = {k: statistics.median(v) for k, v in parts.items()} + out["total"] = sum(out.values()) + return out + + +def bench_workload(name, n_pairs, threads, reps, biopython, emit): + A, B, y, params, alphabet, mode, gap_mode, substitution_mode = make_workload(name, n_pairs) + for t in threads: + engine = NwgradEngine(A, B, mode, gap_mode, substitution_mode, alphabet, t) + emit({"kind": "iteration", "workload": name, "pairs": n_pairs, "backend": "nwgrad", + "threads": t, **time_iterations(engine, params, y, reps)}) + if biopython: + engine = _BiopythonEngine(A, B, make_aligner(mode, params), gap_mode, + substitution_mode, alphabet, 1) + emit({"kind": "iteration", "workload": name, "pairs": n_pairs, "backend": "biopython", + "threads": 1, **time_iterations(engine, params, y, max(1, reps // 2))}) + baseline = default_baseline(mode, gap_mode, substitution_mode, alphabet) + t0 = time.perf_counter() + if biopython: + get_initial_estimate(_align_pairs(A, B, baseline, 1), y, substitution_mode, gap_mode, alphabet) + t1 = time.perf_counter() + engine = NwgradEngine(A, B, mode, gap_mode, substitution_mode, alphabet, 1) + engine.set_params(baseline_parameters(baseline, gap_mode, alphabet), substitution_mode="general") + engine.scores() + get_initial_estimate_from_counts(engine.raw_counts(), y, substitution_mode, gap_mode, alphabet) + t2 = time.perf_counter() + emit({"kind": "initial_estimate", "workload": name, "pairs": n_pairs, "threads": 1, + "biopython": (t1 - t0) if biopython else None, "nwgrad": t2 - t1}) + + +# --- fingerprints ------------------------------------------------------------- + +def sha(*arrays): + h = hashlib.sha256() + for a in arrays: + h.update(np.ascontiguousarray(np.asarray(a, dtype=np.float64)).tobytes()) + return h.hexdigest()[:16] + + +def fingerprints(emit): + """ + Hashes of nwgrad's scores and weighted gradient, and of a 10-iteration fit + (fitted parameters and intercept), from fixed portable inputs. The fit's + reported log-likelihood is left out: numpy may sum it in a different order + on different CPUs. + """ + for name in ("mirna-local", "mirna-global"): + A, B, y, params, alphabet, mode, gap_mode, substitution_mode = make_workload(name, 2000, seed=7) + record = {"kind": "fingerprint", "workload": name} + weights = np.array([2 * k / (len(A) - 1) - 1 for k in range(len(A))]) + for t in (1, 4): + engine = NwgradEngine(A, B, mode, gap_mode, substitution_mode, alphabet, t) + engine.set_params(params) + scores = engine.scores() + g = engine.batch.weighted_grad(weights).to_dict() + record[f"engine_threads_{t}"] = sha(scores, g["matrix"], + [g[k] for k in ("gap_open_a", "gap_extend_a", + "gap_open_b", "gap_extend_b")]) + result = discrimalign(A, B, y, aligner_mode=mode, gap_mode=gap_mode, + substitution_mode=substitution_mode, initial_parameters=params, + max_iter=10, stepfunction=create_powerstep(1e-3), + return_alignments=False, backend="nwgrad", num_threads=1) + keys = [k for k in ("substitution_matrix", "match_score", "mismatch_score", + "open_gap_score", "extend_gap_score", "gap_score") if k in result] + record["fit"] = sha(*[result[k] for k in keys], result["alpha"]) + emit(record) + + +# --- main --------------------------------------------------------------------- + +def default_threads(): + n = os.cpu_count() or 1 + threads, t = [], 1 + while t < n: + threads.append(t) + t *= 2 + return threads + [n, 0] + + +def summarize(records): + lines = [] + for name in WORKLOADS: + rows = [r for r in records if r["kind"] == "iteration" and r["workload"] == name] + if not rows: + continue + bio = next((r for r in rows if r["backend"] == "biopython"), None) + lines.append(f"\n{name}, {rows[0]['pairs']} pairs: seconds per iteration") + lines.append(f" {'backend':10s} {'threads':>7s} {'align':>8s} {'alpha':>8s} {'grad':>8s} " + f"{'other':>8s} {'total':>8s} {'vs Biopython':>13s}") + for r in rows: + label = "auto" if r["threads"] == 0 else str(r["threads"]) + speedup = f"{bio['total'] / r['total']:.1f}x" if bio else "-" + lines.append(f" {r['backend']:10s} {label:>7s} {r['align']:8.3f} {r['alpha']:8.3f} " + f"{r['grad']:8.3f} {r['other']:8.3f} {r['total']:8.3f} {speedup:>13s}") + for r in records: + if r["kind"] == "initial_estimate" and r["workload"] == name: + bio_s = f"{r['biopython']:.2f} s" if r["biopython"] is not None else "-" + lines.append(f" initial estimate (1 thread): Biopython {bio_s}, nwgrad {r['nwgrad']:.2f} s") + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0].strip()) + parser.add_argument("--workloads", default=",".join(WORKLOADS), + help=f"comma-separated subset of: {', '.join(WORKLOADS)}") + parser.add_argument("--pairs", type=int, help="pair count for every workload " + "(default: 10000 for miRNA-sized pairs, 2000 for proteins)") + parser.add_argument("--threads", help="comma-separated nwgrad thread counts; 0 means " + "automatic (default: powers of 2 up to os.cpu_count(), that count, and 0)") + parser.add_argument("--reps", type=int, default=5, help="timed iterations per measurement") + parser.add_argument("--quick", action="store_true", help="a tenth of the pairs, 2 repetitions") + parser.add_argument("--no-biopython", action="store_true", help="skip the Biopython backend") + parser.add_argument("--fingerprint", action="store_true", + help="only print fingerprints for comparing results across machines") + parser.add_argument("--json", help="also write the records to this file") + args = parser.parse_args() + warnings.simplefilter("ignore") + + records = [] + sink = open(args.json, "w") if args.json else None + + def emit(record): + records.append(record) + line = json.dumps(record) + print(line, flush=True) + if sink: + sink.write(line + "\n") + sink.flush() + + emit({"kind": "environment", "cpu_count": os.cpu_count(), "machine": platform.machine(), + "system": platform.system(), "python": platform.python_version(), + "nwgrad_isa": nwgrad.simd_isa(), "nwgrad_compiled_with": nwgrad.compiled_with(), + "numpy": np.__version__}) + if args.fingerprint: + fingerprints(emit) + return + threads = [int(t) for t in args.threads.split(",")] if args.threads else default_threads() + reps = 2 if args.quick else args.reps + for name in args.workloads.split(","): + n = args.pairs or WORKLOADS[name][5] + if args.quick and not args.pairs: + n //= 10 + bench_workload(name, n, threads, reps, not args.no_biopython, emit) + print(summarize(records)) + if sink: + sink.close() + + +if __name__ == "__main__": + main() diff --git a/case_study_for_mirna/README.md b/case_study_for_mirna/README.md index bc530ec..646c75a 100644 --- a/case_study_for_mirna/README.md +++ b/case_study_for_mirna/README.md @@ -155,3 +155,29 @@ uv run python case_study_for_mirna/case_study_mirna.py \ ``` The metrics for the user-provided set are added to the same output files as the miRBench evaluation splits. + +## Inference with bundled trained models + +The repository includes two fitted miRNA case-study models: + +- `case_study_for_mirna/trained_models/manakov_best_model.pkl` +- `case_study_for_mirna/trained_models/hejret_best_model.pkl` + +Create a CSV with sequence-pair columns, or use the ready-to-run example at `examples/mirna_pairs.csv`: + +```csv +id,sequence_a,sequence_b +pair_1,AUGCUA,AUGGUA +pair_2,CUGA,CUGU +``` + +Run inference with either built-in model alias: + +```bash +uv run python -m src.infer --model manakov --input examples/mirna_pairs.csv --output predictions_manakov.csv +uv run python -m src.infer --model hejret --input examples/mirna_pairs.csv --output predictions_hejret.csv +``` + +The output CSV includes the original sequences, normalized sequences, alignment score, logistic probability, aligned sequences, alignment markers, and per-position operations. The default `--normalize auto` converts `U`/`T` as needed to match the trained model alphabet. + +If your input uses manuscript-style columns where `gene` is the target sequence before reverse-complementing, pass `--seq-a-column noncodingRNA --seq-b-column gene --reverse-complement-b` so inference matches the case-study preprocessing. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..bbd64fb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,20 @@ +# Example Inference Inputs + +`mirna_pairs.csv` is a small ready-to-run input file for testing DiscrimAlign miRNA inference. + +Run it with a bundled trained model: + +```bash +uv run python -m src.infer \ + --model manakov \ + --input examples/mirna_pairs.csv \ + --output predictions_manakov.csv +``` + +The input schema is: + +- `id`: optional identifier preserved in the output +- `sequence_a`: first sequence +- `sequence_b`: second sequence + +The output adds probability, alignment score, normalized sequences, aligned sequences, alignment markers, and per-position operations. diff --git a/examples/mirna_pairs.csv b/examples/mirna_pairs.csv new file mode 100644 index 0000000..81bf54a --- /dev/null +++ b/examples/mirna_pairs.csv @@ -0,0 +1,5 @@ +id,sequence_a,sequence_b +example_positive_like,AUGCUA,AUGGUA +example_short,CUGA,CUGU +example_mismatch,AUGCUA,CUGU +example_dna_input,ATGCTA,ATGGTA diff --git a/pyproject.toml b/pyproject.toml index 7f32b0f..a2dd734 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "discrimalign" version = "0.1.0" description = "Discriminative alignment parameter learning from labelled biological sequence pairs" readme = "README.md" -requires-python = ">=3.10,<3.13" +requires-python = ">=3.10" dependencies = [ "biopython>=1.86", "ipykernel>=6.29.0", @@ -11,6 +11,7 @@ dependencies = [ "jupyterlab>=4.2.0", "matplotlib>=3.9.2", "mirbench", + "nwgrad>=0.5.2", "numpy>=1.26.4", "pandas>=2.2.2", "scikit-learn>=1.4.2", @@ -19,4 +20,16 @@ dependencies = [ ] [tool.uv] -package = false \ No newline at end of file +package = false + +[dependency-groups] +dev = [ + "pytest>=8", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +markers = [ + "slow: end-to-end runs on larger data (deselect with -m 'not slow'); the miRBench runs also need DISCRIMALIGN_RUN_MIRBENCH=1", +] diff --git a/src/__init__.py b/src/__init__.py index 1a23573..458e5fe 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +1,11 @@ from .discrimalign import discrimalign +from .inference import load_model, predict_csv, predict_pairs, save_model, summarize_alignment + +__all__ = [ + "discrimalign", + "load_model", + "predict_csv", + "predict_pairs", + "save_model", + "summarize_alignment", +] diff --git a/src/discrimalign.py b/src/discrimalign.py index c1f9d06..4f352f7 100644 --- a/src/discrimalign.py +++ b/src/discrimalign.py @@ -5,11 +5,13 @@ from Bio.Align import PairwiseAligner, substitution_matrices import numpy as np from numpy import random as rd -from scipy.optimize import minimize from copy import deepcopy from math import ceil -from .optimization import get_initial_estimate, get_first_alignment -from .logit_link import logit_partial_scores, logit_logL, logit_subgradient +import os +from .optimization import (create_powerstep, get_initial_estimate, + get_initial_estimate_from_counts, get_first_alignment) +from .logit_link import (_logit_logL_unchecked, logistic_step, logit_partial_scores, + logit_subgradient) def _align_pair_chunk(pair_chunk, aligner): @@ -72,6 +74,86 @@ def _warm_start_alphabet(baseline_aligner=None, initial_parameters=None): return None +def _resolve_alphabet(alphabet, seqlistA, seqlistB, baseline_aligner=None, + initial_parameters=None): + """ + The alphabet of the fit: the one given; else a warm start's (the + substitution matrix of initial_parameters, then of baseline_aligner); else + the sorted characters of the sequences. + """ + if alphabet is not None: + return alphabet + alphabet = _warm_start_alphabet(baseline_aligner, initial_parameters) + if alphabet is not None: + return alphabet + return ''.join(sorted({char for seqlist in (seqlistA, seqlistB) + for seq in seqlist for char in seq})) + + +def _configure_aligner(aligner, params, gap_mode, substitution_mode): + if gap_mode == 'affine': + aligner.open_gap_score = params['open_gap_score'] + aligner.extend_gap_score = params['extend_gap_score'] + elif gap_mode == 'linear': + aligner.gap_score = params['gap_score'] + + if substitution_mode == 'simple': + aligner.match_score = params['match_score'] + aligner.mismatch_score = params['mismatch_score'] + else: + aligner.substitution_matrix = params['substitution_matrix'] + + +# Gap scores are costs and are kept at or below this value. A positive gap +# score makes local alignment ill-posed: Biopython's align() and score() +# disagree there, and the two backends end local alignments differently. +# TODO: reset to 0.0 once the backends handle the kink at a gap score of 0. +# There, zero-cost gap columns tie with none, and Biopython and nwgrad break +# the tie differently, so they return different (both valid) subgradients. +# Out of scope for the nwgrad integration. -1e-4 keeps the fit off the kink +# and well above PairwiseAligner.epsilon (1e-6), Biopython's tie tolerance. +_MAX_GAP_SCORE = -1e-4 + + +def _clip_gap_scores(params, gap_mode): + """Project gap scores onto <= _MAX_GAP_SCORE.""" + keys = ('open_gap_score', 'extend_gap_score') if gap_mode == 'affine' else ('gap_score',) + for key in keys: + params[key] = min(params[key], _MAX_GAP_SCORE) + + +class _BiopythonEngine: + """ + Scores and subgradients from Biopython alignments: set_params(), then + scores(), then raw_subgradient() for the alignments just computed. + """ + + def __init__(self, seqlistA, seqlistB, aligner, gap_mode, substitution_mode, + alphabet, num_threads): + self.seqlistA = seqlistA + self.seqlistB = seqlistB + self.aligner = aligner + self.gap_mode = gap_mode + self.substitution_mode = substitution_mode + self.alphabet = alphabet + self.num_threads = num_threads + self.alignments = None + + def set_params(self, params): + _configure_aligner(self.aligner, params, self.gap_mode, self.substitution_mode) + + def scores(self): + self.alignments = _align_pairs(self.seqlistA, self.seqlistB, self.aligner, self.num_threads) + return [aln.score for aln in self.alignments] + + def raw_subgradient(self, logit_scores, labels, alpha): + return logit_subgradient(self.alignments, logit_scores, labels, alpha, self.alphabet) + + def logistic_step(self, labels, alpha0, alpha_solver): + """Align, then one iteration's logistic work in numpy; see logit_link.logistic_step.""" + return logistic_step(self, labels, alpha0, alpha_solver) + + def discrimalign(seqlistA, seqlistB, labels, baseline_aligner=None, @@ -82,22 +164,75 @@ def discrimalign(seqlistA, seqlistB, stochastic_factor=None, stepfunction=None, max_iter=1000, tol=1e-3, - num_threads=1, + num_threads=0, subgradient_scale=1.0, initial_parameters=None, return_alignments=True, - verbose=False): + verbose=False, + backend='nwgrad', + alpha_solver='safeguarded_newton', + nwgrad_fill='striped'): + """ + backend selects where alignment scores and subgradients come from: + 'nwgrad' (the default) or 'biopython' (PairwiseAligner). With 'nwgrad', the initial + estimate is fitted on nwgrad's alignments of the baseline too, and a + baseline_aligner must have uniform gap scores and no wildcard. The + returned aligner is a Biopython PairwiseAligner, and the returned alignments + are Biopython Alignment objects; with 'nwgrad' they are nwgrad's own paths. + + num_threads=0 (the default) picks the thread count automatically: all + logical cores with 'nwgrad', 1 with 'biopython', whose threads contend + for the GIL and only slow it down. + + alpha_solver selects how the intercept alpha is fitted at each iteration: + 'safeguarded_newton' (the default) finds the exact optimum with fit_alpha() + in logit_link; 'bfgs' is the previous scipy.optimize.minimize (BFGS) fit, + which can stop far from the optimum when alpha moves a long way between + iterations, e.g. with subgradient_scale=1 on large data. + + nwgrad_fill selects nwgrad's vectorized DP fill: 'striped' (the default), + 'rowwise' or 'interpair'. All three give the same scores, gradients and fit, + bit for bit. On short pairs such as miRNA-target sites, 'rowwise' is about 2x + faster than 'striped', and 'interpair' (several pairs per vector) faster + still. + """ # TODO: Implement tol and additional stepfunctions. + assert backend in {'biopython', 'nwgrad'} + assert alpha_solver in {'safeguarded_newton', 'bfgs'} assert aligner_mode in {'local', 'global'} assert gap_mode in {'affine', 'linear'} assert substitution_mode in {'general', 'symmetric', 'simple'} - if alphabet is None: - alphabet = _warm_start_alphabet(baseline_aligner, initial_parameters) - if alphabet is None: - charsetA = set(char for seq in seqlistA for char in seq) - charsetB = set(char for seq in seqlistB for char in seq) - alphabet = charsetA | charsetB - alphabet = ''.join(sorted(alphabet)) + if nwgrad_fill not in {'striped', 'rowwise', 'interpair'}: + raise ValueError("nwgrad_fill must be 'striped', 'rowwise' or 'interpair', " + f"got {nwgrad_fill!r}") + if nwgrad_fill != 'striped' and backend != 'nwgrad': + raise ValueError("nwgrad_fill applies to the nwgrad backend only") + if stepfunction is None: + stepfunction = create_powerstep(1e-4) + for seqlist, name in ((seqlistA, 'seqlistA'), (seqlistB, 'seqlistB')): + empty = [i for i, seq in enumerate(seqlist) if len(seq) == 0] + if empty: + raise ValueError(f'{name} contains empty sequences (at indices {empty[:10]}); ' + 'every sequence needs at least one residue') + # The labels are checked once, here; the iterations use them as floats unchecked. + labels_float = np.asarray(labels, dtype=float) + if not np.isin(labels_float, [0, 1]).all(): + raise ValueError('Labels can only be 0 or 1') + positives = np.sum(labels_float) + if positives == 0 or positives == len(labels_float): + raise ValueError('Labels of both classes are needed: with one class the ' + 'likelihood has no finite maximum') + if num_threads == 0 and backend == 'biopython': + num_threads = 1 + elif num_threads == 0: + # All logical cores, passed to nwgrad explicitly rather than its own + # n_threads=0, which picks physical cores: short pairs such as + # miRNA-target sites gain about 25% from SMT on hosts that have it. + # TODO: long-pair (e.g. protein) workflows can be faster on physical + # cores once their DP tables outgrow the cache; see TODO.md in nwgrad. + num_threads = os.cpu_count() or 1 + alphabet = _resolve_alphabet(alphabet, seqlistA, seqlistB, baseline_aligner, + initial_parameters) if verbose: print('Alphabet:') @@ -138,13 +273,29 @@ def add_noise(shape, niter): aligner.substitution_matrix = substitution_matrices.Array(data=9*np.eye(len(alphabet))-4, alphabet=alphabet) - alnlist = _align_pairs(seqlistA, seqlistB, aligner, num_threads) - alignment_scores = [aln.score for aln in alnlist] + # The baseline aligner's mode, when given, takes precedence over aligner_mode. + if backend == 'nwgrad': + from .nwgrad_backend import NwgradEngine, baseline_parameters + engine = NwgradEngine(seqlistA, seqlistB, aligner.mode, gap_mode, + substitution_mode, alphabet, num_threads, fill=nwgrad_fill) + else: + engine = _BiopythonEngine(seqlistA, seqlistB, aligner, gap_mode, + substitution_mode, alphabet, num_threads) - # Initial logistic estimation, unless caller provides fitted parameters for a warm start. + # Initial logistic estimation from the baseline alignments, unless the + # caller provides fitted parameters for a warm start. if initial_parameters is not None: updated_parameters = deepcopy(initial_parameters) + elif backend == 'nwgrad': + engine.set_params(baseline_parameters(aligner, gap_mode, alphabet), + substitution_mode='general') + engine.scores() + updated_parameters = get_initial_estimate_from_counts(engine.count_arrays(), labels, + substitution_mode=substitution_mode, + gap_mode=gap_mode, + alphabet=alphabet) else: + alnlist = _align_pairs(seqlistA, seqlistB, aligner, num_threads) updated_parameters = get_initial_estimate(alnlist, labels, substitution_mode=substitution_mode, gap_mode=gap_mode, @@ -152,17 +303,8 @@ def add_noise(shape, niter): if verbose: print('Initial parameters:') print(updated_parameters) - if gap_mode == 'affine': - aligner.open_gap_score = updated_parameters['open_gap_score'] - aligner.extend_gap_score = updated_parameters['extend_gap_score'] - else: - aligner.gap_score = updated_parameters['gap_score'] - - if substitution_mode == 'simple': - aligner.match_score = updated_parameters['match_score'] - aligner.mismatch_score = updated_parameters['mismatch_score'] - else: - aligner.substitution_matrix = updated_parameters['substitution_matrix'] + _clip_gap_scores(updated_parameters, gap_mode) + _configure_aligner(aligner, updated_parameters, gap_mode, substitution_mode) # Subgradient refinement loglik_trajectory = [] @@ -172,71 +314,17 @@ def add_noise(shape, niter): for iternb in range(max_iter): if verbose: print('Start of iteration', iternb) - # Set new aligner parameters - if gap_mode == 'affine': - aligner.open_gap_score = updated_parameters['open_gap_score'] - aligner.extend_gap_score = updated_parameters['extend_gap_score'] - elif gap_mode == 'linear': - aligner.gap_score = updated_parameters['gap_score'] - - if substitution_mode == 'simple': - aligner.match_score = updated_parameters['match_score'] - aligner.mismatch_score = updated_parameters['mismatch_score'] - else: - aligner.substitution_matrix = updated_parameters['substitution_matrix'] - - # Realign with the new parameters - alnlist = _align_pairs(seqlistA, seqlistB, aligner, num_threads) - alignment_scores = [aln.score for aln in alnlist] - logit_scores = logit_partial_scores(alignment_scores, - updated_parameters['alpha']) - new_logL = logit_logL(logit_scores, labels) + # Realign with the new parameters, then the log-likelihood at the + # current alpha, the alpha fit and the subgradient at the new alpha. + engine.set_params(updated_parameters) + new_logL, new_alpha, subgradient = engine.logistic_step( + labels_float, updated_parameters['alpha'], alpha_solver) loglik_trajectory.append(new_logL) if verbose: print("Current alpha:", updated_parameters['alpha']) print('Current logL:', new_logL) -## EL = 0 -## VL = 0 -## for ls in logit_scores: -## if 1e-30 < ls < 1-1e-30: -## EL += ls*np.log(ls) + (1-ls)*np.log(1-ls) -## VL += ls*(1-ls)*(np.log(ls)**2 + np.log(1-ls)**2) -## SDL = np.sqrt(VL) -## loglik_expectation.append(EL) -## loglik_sd.append(SDL) - - # Optimize the logistic intercept (alpha) - def alpha_target(alpha): - logit_scores = logit_partial_scores(alignment_scores, alpha) - return -logit_logL(logit_scores, labels) - - def alpha_fprime(alpha): - logit_scores = logit_partial_scores(alignment_scores, alpha) - return -np.sum(labels - logit_scores) - - def alpha_fsec(alpha): - logit_scores = logit_partial_scores(alignment_scores, alpha) - return np.sum(logit_scores*(1 - logit_scores)) - - new_alpha = minimize(alpha_target, - updated_parameters['alpha'], - jac=alpha_fprime, - # hess=alpha_fsec, - # method='Newton-CG' - )['x'][0] - - logit_scores = logit_partial_scores(alignment_scores, new_alpha) - if verbose: - new_logL = logit_logL(logit_scores, labels) print("Updated alpha:", new_alpha) - print('Updated logL:', new_logL) - updated_parameters['alpha'] = new_alpha - - # Make a subgradient step - subgradient = logit_subgradient(alnlist, logit_scores, - labels, new_alpha, - alphabet) if subgradient_scale != 1.0: subgradient['Gap opens'] *= subgradient_scale subgradient['Gap extends'] *= subgradient_scale @@ -289,6 +377,7 @@ def alpha_fsec(alpha): subsM = subsM + add_noise(subsM.shape, iternb) updated_parameters['substitution_matrix'] += stepsize*subsM subgradient_square_norm += np.sum(subsM**2) + _clip_gap_scores(updated_parameters, gap_mode) subgradient_l2_trajectory.append(np.sqrt(subgradient_square_norm)) if verbose: print('New parameters:') @@ -319,11 +408,16 @@ def alpha_fsec(alpha): results['substitution_matrix'] = updated_parameters['substitution_matrix'] # Realign with the new parameters - alnlist = _align_pairs(seqlistA, seqlistB, aligner, num_threads) - alignment_scores = [aln.score for aln in alnlist] + engine.set_params(updated_parameters) + alignment_scores = engine.scores() + if backend == 'nwgrad': + alnlist = engine.alignments() if return_alignments else None + else: + alnlist = engine.alignments logit_scores = logit_partial_scores(alignment_scores, updated_parameters['alpha']) - new_logL = logit_logL(logit_scores, labels) + new_logL = _logit_logL_unchecked(np.asarray(alignment_scores, dtype=float), + updated_parameters['alpha'], labels_float) ## EL = 0 ## VL = 0 ## for ls in logit_scores: @@ -342,6 +436,7 @@ def alpha_fsec(alpha): results['alignments'] = alnlist results['alignment_logit_scores'] = logit_scores results['alpha'] = updated_parameters['alpha'] + results['alphabet'] = alphabet # results['loglik_expectation_trajectory'] = loglik_expectation # results['loglik_sd_trajectory'] = loglik_sd return results diff --git a/src/infer.py b/src/infer.py new file mode 100644 index 0000000..72f4cc7 --- /dev/null +++ b/src/infer.py @@ -0,0 +1,58 @@ +"""Command-line CSV inference for saved DiscrimAlign models.""" + +import argparse +from pathlib import Path + +from .inference import load_model, predict_csv + +MODEL_ALIASES = { + "manakov": Path("case_study_for_mirna/trained_models/manakov_best_model.pkl"), + "hejret": Path("case_study_for_mirna/trained_models/hejret_best_model.pkl"), +} + + +def _resolve_model_path(model): + return MODEL_ALIASES.get(model, Path(model)) + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Run DiscrimAlign inference on sequence-pair CSV data.") + parser.add_argument( + "--model", + required=True, + help="Model path, or one of the built-in miRNA aliases: manakov, hejret.", + ) + parser.add_argument("--input", required=True, help="Input CSV containing sequence pairs.") + parser.add_argument("--output", required=True, help="Output CSV for predictions.") + parser.add_argument("--seq-a-column", default="sequence_a", help="Column name for first sequence.") + parser.add_argument("--seq-b-column", default="sequence_b", help="Column name for second sequence.") + parser.add_argument( + "--normalize", + default="auto", + choices=["auto", "none"], + help="Normalize U/T to match the trained model alphabet.", + ) + parser.add_argument( + "--reverse-complement-b", + action="store_true", + help="Reverse-complement the second sequence column before scoring.", + ) + args = parser.parse_args(argv) + + model_path = _resolve_model_path(args.model) + model = load_model(model_path) + rows = predict_csv( + input_csv=args.input, + output_csv=args.output, + model=model, + sequence_a_column=args.seq_a_column, + sequence_b_column=args.seq_b_column, + normalize=args.normalize, + reverse_complement_b=args.reverse_complement_b, + ) + print(f"Loaded model from {model_path}") + print(f"Wrote {len(rows)} prediction rows to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/src/inference.py b/src/inference.py new file mode 100644 index 0000000..97b7a06 --- /dev/null +++ b/src/inference.py @@ -0,0 +1,220 @@ +"""Inference helpers for trained DiscrimAlign models.""" + +import csv +import pickle +from pathlib import Path + +from Bio.Seq import Seq + +from .logit_link import logit_partial_scores +from .optimization import get_first_alignment + + +def _model_parts(model): + if not isinstance(model, dict): + raise TypeError("model must be a discrimalign result dictionary") + if "aligner" not in model: + raise ValueError("model must contain an 'aligner' entry") + if "alpha" in model: + alpha = model["alpha"] + elif isinstance(model.get("summary"), dict) and "alpha" in model["summary"]: + alpha = model["summary"]["alpha"] + else: + raise ValueError("model must contain 'alpha' or 'summary[alpha]' entries") + return model["aligner"], alpha + + +def _model_alphabet(aligner): + matrix = getattr(aligner, "substitution_matrix", None) + alphabet = getattr(matrix, "alphabet", None) + if alphabet is None: + return None + return set(alphabet) + + +def _normalize_sequence(sequence, alphabet, normalize): + if normalize in (None, False, "none"): + return sequence + if normalize != "auto": + raise ValueError("normalize must be 'auto', 'none', None, or False") + if alphabet and "T" in alphabet and "U" not in alphabet: + return sequence.replace("U", "T").replace("u", "t") + if alphabet and "U" in alphabet and "T" not in alphabet: + return sequence.replace("T", "U").replace("t", "u") + return sequence + + +def _reverse_complement(sequence): + return str(Seq(sequence).reverse_complement()) + + +def _alignment_rows(alignment): + target = str(alignment[0]) + query = str(alignment[1]) + markers = [] + operations = [] + for target_char, query_char in zip(target, query): + if target_char == "-" or query_char == "-": + marker = "-" + operation = "gap" + elif target_char == query_char: + marker = "|" + operation = "match" + else: + marker = "." + operation = "mismatch" + markers.append(marker) + operations.append(operation) + return target, "".join(markers), query, operations + + +def summarize_alignment(alignment): + """Return a serializable summary of a Biopython alignment.""" + target, markers, query, operations = _alignment_rows(alignment) + return { + "aligned_sequence_a": target, + "alignment_marks": markers, + "aligned_sequence_b": query, + "operations": operations, + "text": str(alignment), + } + + +def predict_pairs( + seqlistA, + seqlistB, + model, + return_alignments=True, + normalize="auto", + reverse_complement_b=False, +): + """Score sequence pairs with a fitted DiscrimAlign model. + + Parameters + ---------- + seqlistA, seqlistB : iterable of str + Sequence pairs to align and score. + model : dict + Result dictionary returned by ``discrimalign``. + return_alignments : bool, default=True + Include text and per-position alignment summaries in each row. + normalize : {"auto", "none"}, default="auto" + Convert U/T automatically when the fitted model alphabet requires it. + reverse_complement_b : bool, default=False + Reverse-complement each second sequence before normalization and scoring. + """ + seqlistA = list(seqlistA) + seqlistB = list(seqlistB) + if len(seqlistA) != len(seqlistB): + raise ValueError("seqlistA and seqlistB must have the same length") + + aligner, alpha = _model_parts(model) + alphabet = _model_alphabet(aligner) + rows = [] + for index, (seqA, seqB) in enumerate(zip(seqlistA, seqlistB)): + transformed_seqB = _reverse_complement(seqB) if reverse_complement_b else seqB + normalized_seqA = _normalize_sequence(seqA, alphabet, normalize) + normalized_seqB = _normalize_sequence(transformed_seqB, alphabet, normalize) + alignment = get_first_alignment(normalized_seqA, normalized_seqB, aligner) + probability = float(logit_partial_scores([alignment.score], alpha)[0]) + row = { + "index": index, + "sequence_a": seqA, + "sequence_b": seqB, + "normalized_sequence_a": normalized_seqA, + "normalized_sequence_b": normalized_seqB, + "alignment_score": float(alignment.score), + "probability": probability, + } + if return_alignments: + row.update(summarize_alignment(alignment)) + rows.append(row) + return rows + + +def save_model(model, path): + """Save a fitted DiscrimAlign result dictionary for later inference.""" + _model_parts(model) + path = Path(path) + with path.open("wb") as output_file: + pickle.dump(model, output_file) + + +def load_model(path): + """Load a fitted DiscrimAlign result dictionary saved with ``save_model``.""" + path = Path(path) + with path.open("rb") as input_file: + model = pickle.load(input_file) + _model_parts(model) + return model + + +def predict_csv( + input_csv, + output_csv, + model, + sequence_a_column="sequence_a", + sequence_b_column="sequence_b", + normalize="auto", + reverse_complement_b=False, +): + """Run inference from a CSV file and write prediction rows to another CSV.""" + input_csv = Path(input_csv) + output_csv = Path(output_csv) + with input_csv.open(newline="") as input_file: + reader = csv.DictReader(input_file) + if reader.fieldnames is None: + raise ValueError("input CSV must include a header row") + missing_columns = { + column + for column in (sequence_a_column, sequence_b_column) + if column not in reader.fieldnames + } + if missing_columns: + missing = ", ".join(sorted(missing_columns)) + raise ValueError(f"input CSV is missing required columns: {missing}") + input_rows = list(reader) + + predictions = predict_pairs( + [row[sequence_a_column] for row in input_rows], + [row[sequence_b_column] for row in input_rows], + model, + normalize=normalize, + reverse_complement_b=reverse_complement_b, + ) + output_rows = [] + for input_row, prediction in zip(input_rows, predictions): + output_rows.append( + { + **input_row, + "normalized_sequence_a": prediction["normalized_sequence_a"], + "normalized_sequence_b": prediction["normalized_sequence_b"], + "alignment_score": prediction["alignment_score"], + "probability": prediction["probability"], + "aligned_sequence_a": prediction["aligned_sequence_a"], + "alignment_marks": prediction["alignment_marks"], + "aligned_sequence_b": prediction["aligned_sequence_b"], + "operations": ";".join(prediction["operations"]), + } + ) + + fieldnames = list(input_rows[0].keys()) if input_rows else list(reader.fieldnames) + for fieldname in ( + "normalized_sequence_a", + "normalized_sequence_b", + "alignment_score", + "probability", + "aligned_sequence_a", + "alignment_marks", + "aligned_sequence_b", + "operations", + ): + if fieldname not in fieldnames: + fieldnames.append(fieldname) + + output_csv.parent.mkdir(parents=True, exist_ok=True) + with output_csv.open("w", newline="") as output_file: + writer = csv.DictWriter(output_file, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(output_rows) + return output_rows diff --git a/src/logit_link.py b/src/logit_link.py index 773226b..3fb1671 100644 --- a/src/logit_link.py +++ b/src/logit_link.py @@ -5,6 +5,7 @@ import numpy as np from Bio.Align import substitution_matrices +from scipy.optimize import minimize from scipy.special import expit @@ -17,20 +18,36 @@ def logit_partial_scores(alignment_scores, alpha): return expit(alpha + alignment_scores) -def logit_logL(logit_scores, labels): +def logit_logL(alignment_scores, alpha, labels): """ - Calculate the log likelihood in the logistic model. - logit_scores are a list of values of the logistic function - of the alignment score, calculated with logit_partial_scores. - labels are a 1D numpy array with values 0 or 1. + Calculate the log likelihood in the logistic model: + sum_i y_i z_i - log(1 + e^{z_i}), with z_i = alpha + alignment_scores[i]. + Computed from the logits rather than from the probabilities, so it needs + no clipping and loses no precision on confident predictions, and + log(1 + e^z) is evaluated without overflow. + labels are a 1D array with values 0 or 1. + Raises FloatingPointError if the result is not finite. """ - logit_scores = np.asarray(logit_scores, dtype=float) labels = np.asarray(labels) - eps = np.finfo(float).eps - logit_scores = np.clip(logit_scores, eps, 1.0 - eps) if not np.isin(labels, [0, 1]).all(): raise ValueError('Labels can only be 0 or 1') - return float(np.sum(labels * np.log(logit_scores) + (1 - labels) * np.log1p(-logit_scores))) + return _logit_logL_unchecked(np.asarray(alignment_scores, dtype=float), alpha, + labels.astype(float)) + + +def _logit_logL_unchecked(alignment_scores, alpha, labels): + """ + logit_logL() for a float array of alignment scores and float labels + already known to be 0 or 1. For loops that evaluate it on the same labels + many times. + """ + z = alignment_scores + alpha + with np.errstate(invalid='ignore'): # a non-finite result raises below + logL = float(np.dot(labels, z) - np.sum(np.logaddexp(0.0, z))) + if not np.isfinite(logL): + raise FloatingPointError(f'Log-likelihood is {logL} at alpha={alpha}: ' + 'alignment scores or alpha are not finite') + return logL def dlda(logit_scores, labels): @@ -50,6 +67,122 @@ def d2lda2(logit_scores, labels): return -np.sum(logit_scores*(1-logit_scores)) +def fit_alpha(alignment_scores, labels, alpha0, tol=1e-12, max_newton=8, maxiter=200, + logit_scores0=None): + """ + The intercept alpha that maximises the likelihood for fixed alignment + scores, starting from alpha0. + + This is the root of dlda, which is strictly decreasing in alpha, so it is + unique whenever both classes are present. Plain Newton steps are taken from + alpha0 while they stay small (|step| <= 1), which is the usual case when + alpha0 is the previous iteration's alpha. Otherwise the root is bracketed + and found by Newton steps inside the bracket, with bisection whenever a + Newton step would leave it or converge too slowly (Numerical Recipes' + rtsafe). The log-likelihood itself is never evaluated. + + logit_scores0, if given, must be logit_partial_scores(alignment_scores, + alpha0); a caller that already has them saves one pass over the data. + """ + labels = np.asarray(labels, dtype=float) + positives = np.sum(labels) + if positives == 0 or positives == len(labels): + raise ValueError('Fitting alpha needs labels of both classes: with one class ' + 'the likelihood has no finite maximum') + return _fit_alpha_unchecked(np.asarray(alignment_scores, dtype=float), labels, alpha0, + tol, max_newton, maxiter, logit_scores0) + + +def _fit_alpha_unchecked(alignment_scores, labels, alpha0, tol=1e-12, max_newton=8, + maxiter=200, logit_scores0=None): + """ + fit_alpha() for float arrays of scores and labels, the labels already + known to be 0 or 1 with both classes present. For loops that fit alpha on + the same labels many times. + """ + def derivatives(alpha, logit_scores=None): + if logit_scores is None: + logit_scores = logit_partial_scores(alignment_scores, alpha) + return float(dlda(logit_scores, labels)), float(-d2lda2(logit_scores, labels)) + + alpha = float(alpha0) + for k in range(max_newton): + g, h = derivatives(alpha, logit_scores0 if k == 0 else None) + step = g / h if h > 0 else np.inf + if not np.isfinite(step) or abs(step) > 1.0: + break + alpha += step + if abs(step) <= tol * max(1.0, abs(alpha)): + return alpha + + # Bracket the root: dlda(lo) >= 0 >= dlda(hi). + lo, hi, width = alpha - 1.0, alpha + 1.0, 1.0 + for _ in range(maxiter): + if derivatives(lo)[0] >= 0: + break + lo -= width + width *= 2 + else: + raise RuntimeError('fit_alpha: could not bracket the root') + width = 1.0 + for _ in range(maxiter): + if derivatives(hi)[0] <= 0: + break + hi += width + width *= 2 + else: + raise RuntimeError('fit_alpha: could not bracket the root') + + alpha = min(max(alpha, lo), hi) + dx_old = dx = hi - lo + g, h = derivatives(alpha) + for _ in range(maxiter): + if g == 0.0: + return alpha + if h > 0 and lo <= alpha + g / h <= hi and abs(2 * g) <= abs(dx_old * h): + dx_old, dx = dx, g / h + alpha += dx + else: + dx_old, dx = dx, 0.5 * (hi - lo) + alpha = lo + dx + if abs(dx) <= tol * max(1.0, abs(alpha)): + return alpha + g, h = derivatives(alpha) + if g > 0: + lo = alpha + else: + hi = alpha + raise RuntimeError('fit_alpha: did not converge') + + +def logistic_step(engine, labels, alpha0, alpha_solver='safeguarded_newton'): + """ + One iteration's logistic work on an engine whose parameters are set: + align (engine.scores()), the log-likelihood at alpha0, the alpha that + maximises the likelihood for these scores, and engine.raw_subgradient() at + that alpha. Returns (loglik_at_alpha0, alpha, subgradient). + + labels: float array of 0s and 1s with both classes present, unchecked. + alpha_solver: 'safeguarded_newton' (fit_alpha) or 'bfgs' (scipy's minimize + on the log-likelihood). + """ + alignment_scores = np.asarray(engine.scores(), dtype=float) + loglik = _logit_logL_unchecked(alignment_scores, alpha0, labels) + if alpha_solver == 'safeguarded_newton': + alpha = _fit_alpha_unchecked(alignment_scores, labels, alpha0, + logit_scores0=logit_partial_scores(alignment_scores, alpha0)) + else: + def alpha_target(alpha): + return -_logit_logL_unchecked(alignment_scores, alpha[0], labels) + + def alpha_fprime(alpha): + return -np.sum(labels - logit_partial_scores(alignment_scores, alpha)) + + alpha = minimize(alpha_target, alpha0, jac=alpha_fprime)['x'][0] + logit_scores = logit_partial_scores(alignment_scores, alpha) + return loglik, alpha, engine.raw_subgradient(logit_scores, labels, alpha) + + def logit_subgradient(alignment_list, logit_scores, labels, alpha, alphabet): @@ -76,11 +209,13 @@ def logit_subgradient(alignment_list, logit_scores, else: subgradient['Gap opens'] += weight ingap1 = True + ingap2 = False elif char2 == '-': if ingap2: subgradient['Gap extends'] += weight else: subgradient['Gap opens'] += weight + ingap1 = False ingap2 = True else: ingap1 = False diff --git a/src/nwgrad_backend.py b/src/nwgrad_backend.py new file mode 100644 index 0000000..a86ed6f --- /dev/null +++ b/src/nwgrad_backend.py @@ -0,0 +1,171 @@ +""" +nwgrad backend for discrimalign(): alignment scores and the log-likelihood +subgradient from one nwgrad batch, built on first use and reused across +iterations. +""" +import numpy as np +import nwgrad +import nwgrad.logistic as nwgrad_logistic +from Bio.Align import substitution_matrices + +from .logit_link import _logit_logL_unchecked, logistic_step +from .nwgrad_params import gap_counts, grad_to_raw, to_nwgrad +from .optimization import CountArrays, EmptyLocalAlignment + + +def baseline_parameters(aligner, gap_mode, alphabet): + """ + The scores of a Biopython PairwiseAligner as a DiscrimAlign parameter + dict, with the substitutions always as a matrix over alphabet. Raises + ValueError for anything the model cannot express: an unsupported mode, a + wildcard, gap scores that differ between the two sequences or between + internal and end gaps, affine gaps when gap_mode is 'linear', or a + matrix missing letters of alphabet. + """ + if aligner.mode not in ('local', 'global'): + raise ValueError(f"nwgrad backend: baseline_aligner mode {aligner.mode!r} " + "is not supported (expected 'local' or 'global')") + if aligner.wildcard is not None: + raise ValueError("nwgrad backend: baseline_aligner wildcard is not supported") + try: + if gap_mode == 'affine': + params = {'open_gap_score': aligner.open_gap_score, + 'extend_gap_score': aligner.extend_gap_score} + else: + params = {'gap_score': aligner.gap_score} + except ValueError as error: + raise ValueError( + "nwgrad backend: baseline_aligner gap scores must be the same for both " + "sequences and for internal and end gaps" + + (", and open must equal extend for gap_mode='linear'" + if gap_mode == 'linear' else "")) from error + matrix = aligner.substitution_matrix + if matrix is None: + data = np.full((len(alphabet), len(alphabet)), float(aligner.mismatch_score)) + data[np.diag_indices(len(alphabet))] = aligner.match_score + else: + missing = sorted(set(alphabet) - set(matrix.alphabet)) + if missing: + raise ValueError("nwgrad backend: baseline_aligner substitution matrix " + f"lacks letters {''.join(missing)!r}") + data = np.array([[matrix[a, b] for b in alphabet] for a in alphabet], dtype=float) + params['substitution_matrix'] = substitution_matrices.Array(alphabet=alphabet, data=data) + return params + + +class NwgradEngine: + """ + Same interface as the Biopython engine in discrimalign: set_params(), + then scores(), then raw_subgradient() for the scores just computed. + + Double precision with pointer traceback, so paths are exactly optimal and + scores agree with Biopython's up to summation order. + """ + + def __init__(self, seqlistA, seqlistB, mode, gap_mode, substitution_mode, + alphabet, num_threads, fill='striped'): + self.seqlistA = list(seqlistA) + self.seqlistB = list(seqlistB) + self.mode = mode + self.gap_mode = gap_mode + self.substitution_mode = substitution_mode + self.alphabet = alphabet + self.batch = nwgrad.SeqPairBatchDouble(n_threads=int(num_threads), + traceback='pointers') + self.fill = fill + self.batch.fill = fill + self._built = False + + def set_params(self, params, substitution_mode=None): + """ + substitution_mode overrides the engine's own for this call, e.g. to + align a baseline given as a full matrix in a simple-mode fit. + """ + nw_params = to_nwgrad(params, self.gap_mode, + substitution_mode or self.substitution_mode, self.alphabet) + self._nw_params = nw_params + if self._built: + self.batch.set_params(nw_params) + else: + self.batch.add_many(self.seqlistA, self.seqlistB, nw_params, + gap_model=self.gap_mode, mode=self.mode, + grad_mode='hard') + self._built = True + + def alignments(self, chunk_size=100_000): + """ + Every pair's alignment at the parameters last set: a Bio.Align.Alignment + with .score, or EmptyLocalAlignment for a local pair with no + positive-scoring alignment. These are nwgrad's own paths, the ones the + scores and subgradients come from, so on ties they agree with them, which + a Biopython realignment need not. Recovering a path needs pair-owned DP + tables, so the pairs are aligned chunk_size at a time to bound memory. + """ + from Bio.Align import Alignment + out = [] + for start in range(0, len(self.seqlistA), chunk_size): + seqs_a = self.seqlistA[start:start + chunk_size] + seqs_b = self.seqlistB[start:start + chunk_size] + batch = nwgrad.SeqPairBatchDouble(n_threads=self.batch.n_threads, + traceback='pointers') + batch.fill = self.fill + batch.add_many(seqs_a, seqs_b, self._nw_params, gap_model=self.gap_mode, + mode=self.mode, grad_mode='none') + batch.alloc_dp() + batch.align_full() + for i, (seq_a, seq_b) in enumerate(zip(seqs_a, seqs_b)): + pair = batch[i] + coordinates = pair.coordinates() + if (coordinates[:, 0] == coordinates[:, -1]).all(): + out.append(EmptyLocalAlignment()) + continue + alignment = Alignment([seq_a, seq_b], coordinates) + alignment.score = pair.score + out.append(alignment) + return out + + def scores(self): + self.batch.score_and_grad() + return self.batch.scores() + + def logistic_step(self, labels, alpha0, alpha_solver): + """ + Align, then one iteration's logistic work: the log-likelihood at + alpha0, the fitted alpha, and the raw subgradient at that alpha, in + logit_subgradient's format; see logit_link.logistic_step. With + alpha_solver='safeguarded_newton', the alpha fit (as fit_alpha()) and + the subgradient run in nwgrad (C++, parallel), and the log-likelihood + comes from logit_logL's formula on the cached scores instead of + nwgrad's, which clips probabilities. Other solvers run in numpy. + labels: float64 array of 0s and 1s. + """ + if alpha_solver != 'safeguarded_newton': + return logistic_step(self, labels, alpha0, alpha_solver) + self.batch.score_and_grad() + step = nwgrad_logistic.step(self.batch, labels, alpha0) + loglik = _logit_logL_unchecked(self.batch.scores(), alpha0, labels) + return loglik, step.alpha, grad_to_raw(step.grad) + + def raw_counts(self): + """Per-pair counts in logit_subgradient's format, for the scores just computed.""" + return [grad_to_raw(self.batch[i].grad) for i in range(len(self.batch))] + + def count_arrays(self): + """ + The same per-pair counts as raw_counts(), as CountArrays, from one + SeqPairBatch.grads() call. + """ + matrices, gaps = self.batch.grads() + gap_opens, gap_extends = gap_counts(*gaps.T) + return CountArrays(matrices, gap_opens, gap_extends, self.batch.alphabet) + + def raw_subgradient(self, logit_scores, labels, alpha): + """ + sum_i (label_i - logit_score_i) * counts_i, in logit_subgradient's + format. nwgrad sums the cached per-pair gradients in its own + parametrization, and the sum is converted once, which is exact + because the conversion is linear. alpha is unused, as in + logit_subgradient. + """ + weights = np.asarray(labels, dtype=float) - np.asarray(logit_scores, dtype=float) + return grad_to_raw(self.batch.weighted_grad(weights)) diff --git a/src/nwgrad_params.py b/src/nwgrad_params.py new file mode 100644 index 0000000..441931f --- /dev/null +++ b/src/nwgrad_params.py @@ -0,0 +1,91 @@ +""" +Conversion between DiscrimAlign's alignment parameters and nwgrad's. + +DiscrimAlign keeps parameters in Biopython's convention: gap values are +negative scores, and a gap of length k scores open + (k-1)*extend. +nwgrad takes positive penalties, a gap of length k costs open + k*extend, +and gaps in sequence A (*_a) and sequence B (*_b) are priced separately. +DiscrimAlign's model has one gap cost for both sequences. +""" +import numpy as np +import nwgrad +from Bio.Align import substitution_matrices + + +def substitution_data(params, substitution_mode, alphabet): + """ + The substitution matrix as a float64 array, and its alphabet. + + In simple mode it is built from match_score and mismatch_score over + alphabet. Otherwise the matrix's own alphabet is used, if it has one. + """ + if substitution_mode == 'simple': + n = len(alphabet) + data = np.full((n, n), float(params['mismatch_score'])) + data[np.diag_indices(n)] = params['match_score'] + return data, alphabet + matrix = params['substitution_matrix'] + matrix_alphabet = getattr(matrix, 'alphabet', None) + if matrix_alphabet is not None: + alphabet = ''.join(matrix_alphabet) + return np.ascontiguousarray(matrix, dtype=np.float64), alphabet + + +def gap_penalties(params, gap_mode): + """(gap_open, gap_extend) as nwgrad penalties, for either sequence.""" + if gap_mode == 'affine': + return (params['extend_gap_score'] - params['open_gap_score'], + -params['extend_gap_score']) + return 0.0, -params['gap_score'] + + +def to_nwgrad(params, gap_mode, substitution_mode, alphabet): + """ + nwgrad.AlignParams equivalent to a DiscrimAlign parameter dict. + + alphabet is used in simple mode, and in the other modes when the + substitution matrix does not carry its own alphabet. + """ + data, alphabet = substitution_data(params, substitution_mode, alphabet) + gap_open, gap_extend = gap_penalties(params, gap_mode) + return nwgrad.AlignParams(nwgrad.SubstMatrix(data, alphabet=alphabet), + gap_open_a=gap_open, gap_extend_a=gap_extend, + gap_open_b=gap_open, gap_extend_b=gap_extend) + + +def grad_to_raw(grad): + """ + An nwgrad gradient in logit_subgradient's output format. + + grad is an nwgrad.AlignParams gradient, or a dict shaped like its + to_dict(). nwgrad reports gap gradients as minus the counts: gap_open is + minus the number of gaps and gap_extend minus the number of gap columns. + Biopython counts the first column of each gap as its open, and the rest + as extends. In linear mode nwgrad only reports gap columns, so all of + them land in 'Gap extends' and only the sum of the two is meaningful. + The conversion is linear, so weighted sums of gradients may be + converted after summing. + """ + if hasattr(grad, 'to_dict'): + grad = grad.to_dict() + gap_opens, gap_extends = gap_counts(*(grad[field] for field in GAP_FIELDS)) + substitutions = substitution_matrices.Array(alphabet=grad['alphabet'], + data=np.array(grad['matrix'], dtype=float)) + return {'Substitutions': substitutions, + 'Gap opens': gap_opens, + 'Gap extends': gap_extends} + + +# nwgrad's gap fields, in the column order of SeqPairBatch.grads(). +GAP_FIELDS = ('gap_open_a', 'gap_extend_a', 'gap_open_b', 'gap_extend_b') + + +def gap_counts(gap_open_a, gap_extend_a, gap_open_b, gap_extend_b): + """ + (gap opens, gap extends) in logit_subgradient's sense, from an nwgrad + gradient's gap fields: numbers, or arrays with one entry per pair. See + grad_to_raw(). + """ + gap_open = gap_open_a + gap_open_b + gap_extend = gap_extend_a + gap_extend_b + return -gap_open, gap_open - gap_extend diff --git a/src/optimization.py b/src/optimization.py index 7bebec4..ba63af3 100644 --- a/src/optimization.py +++ b/src/optimization.py @@ -1,3 +1,5 @@ +from typing import NamedTuple + import numpy as np from sklearn.linear_model import LogisticRegression from Bio.Align import substitution_matrices @@ -35,92 +37,135 @@ def get_first_alignment(seqA, seqB, aligner): raise ### Starting point -def _initial_estim_affinegap_simplesubs(alignment_list, labels): +def _alignment_features(alignment_list, substitution_mode, gap_mode, alphabet): """ - Returns an initial estimator of alignment parameters - using a simple logistic model with intercept and - summary predictors: numbers of matches, mismatches, gap opens and gap extends. + Predictors for the initial estimate, one row per alignment: numbers of + matches and mismatches (simple) or of each substitution (full), then the + numbers of gap opens and gap extends (affine) or of gap columns (linear). """ predictors = [] + if substitution_mode != 'simple': + Asize = len(alphabet) + pair_to_id = {(char1, char2): Asize*i + j for i, char1 in enumerate(alphabet) for j, char2 in enumerate(alphabet)} for aln in alignment_list: counts = aln.counts() - predictors.append( - [ - counts.identities, - counts.mismatches, - counts.open_gaps, - counts.extend_gaps - ] - ) - logit = LogisticRegression(fit_intercept=True, penalty=None) - logit.fit(predictors, labels) - estimates = {'alpha': logit.intercept_[0], - 'match_score': logit.coef_[0][0], - 'mismatch_score': logit.coef_[0][1], - 'open_gap_score': logit.coef_[0][2], - 'extend_gap_score': logit.coef_[0][3]} - return estimates + if gap_mode == 'affine': + gaps = [counts.open_gaps, + counts.extend_gaps] + else: + gaps = [counts.gaps] + if substitution_mode == 'simple': + predictors.append([counts.identities, counts.mismatches] + gaps) + else: + substitutions = [0]*(Asize**2) + for char1, char2 in zip(aln[0], aln[1]): + if char1 != '-' and char2 != '-': + substitutions[pair_to_id[(char1, char2)]] += 1 + predictors.append(substitutions+gaps) + return predictors -def _initial_estim_lineargap_simplesubs(alignment_list, labels): + +class CountArrays(NamedTuple): """ - Returns an initial estimator of alignment parameters - using a simple logistic model with intercept and - summary predictors: numbers of matches, mismatches, gap opens and gap extends. + Per-alignment counts in logit_subgradient's sense, one row per alignment: + substitutions (N, A, A) with rows and columns in the order of alphabet, + and gap_opens and gap_extends (N,). """ - predictors = [] - for aln in alignment_list: - counts = aln.counts() - predictors.append( - [ - counts.identities, - counts.mismatches, - counts.gaps - ] - ) - logit = LogisticRegression(fit_intercept=True, penalty=None) - logit.fit(predictors, labels) - estimates = {'alpha': logit.intercept_[0], - 'match_score': logit.coef_[0][0], - 'mismatch_score': logit.coef_[0][1], - 'gap_score': logit.coef_[0][2]} - return estimates + substitutions: np.ndarray + gap_opens: np.ndarray + gap_extends: np.ndarray + alphabet: str -def _initial_estim_affinegap_fullsubs(alignment_list, labels, - alphabet): + +def _count_arrays_from_raw(raw_counts_list): """ - Returns an initial estimator of alignment parameters - using a Ridge logistic model with intercept and - a full set of predictors: numbers of gap extends and a substitution matrix + CountArrays from a list of counts in logit_subgradient's format, in the + alphabet order of the first one. """ - Asize = len(alphabet) - predictors = [] - pair_to_id = {(char1, char2): Asize*i + j for i, char1 in enumerate(alphabet) for j, char2 in enumerate(alphabet)} - for aln in alignment_list: - counts = aln.counts() - gaps = [counts.open_gaps, - counts.extend_gaps] - substitutions = [0]*(Asize**2) - for char1, char2 in zip(aln[0], aln[1]): - if char1 != '-' and char2 != '-': - substitutions[pair_to_id[(char1, char2)]] += 1 - predictors.append(substitutions+gaps) - - logit = LogisticRegression(fit_intercept=True, solver='newton-cg') - logit.fit(predictors, labels) - substitution_matrix = substitution_matrices.Array(data=np.zeros((Asize, Asize)), - alphabet=alphabet) - for char1 in alphabet: - for char2 in alphabet: - substitution_matrix[char1, char2] = logit.coef_[0][pair_to_id[(char1, char2)]] - open_gap_score = logit.coef_[0][-2] - extend_gap_score = logit.coef_[0][-1] - estimates = {'alpha': logit.intercept_[0], - 'substitution_matrix': substitution_matrix, - 'open_gap_score': open_gap_score, - 'extend_gap_score': extend_gap_score} + n = len(raw_counts_list) + alphabet = ''.join(raw_counts_list[0]['Substitutions'].alphabet) if n else '' + substitutions = np.empty((n, len(alphabet), len(alphabet))) + gap_opens = np.empty(n) + gap_extends = np.empty(n) + for i, raw in enumerate(raw_counts_list): + subs = raw['Substitutions'] + order = [subs.alphabet.index(char) for char in alphabet] + substitutions[i] = np.asarray(subs)[np.ix_(order, order)] + gap_opens[i] = raw['Gap opens'] + gap_extends[i] = raw['Gap extends'] + return CountArrays(substitutions, gap_opens, gap_extends, alphabet) + + +def _count_features(counts, substitution_mode, gap_mode, alphabet): + """ + The same predictors as _alignment_features(), as an (N, p) array, from + per-alignment counts: CountArrays, or a list in logit_subgradient's format + ({'Substitutions', 'Gap opens', 'Gap extends'}), such as nwgrad's per-pair + gradients converted by grad_to_raw(). In linear mode only the total number + of gap columns is used, which is all that nwgrad reports there. + """ + if not isinstance(counts, CountArrays): + counts = _count_arrays_from_raw(counts) + S = counts.substitutions + gaps = ([counts.gap_opens, counts.gap_extends] if gap_mode == 'affine' + else [counts.gap_opens + counts.gap_extends]) + if substitution_mode == 'simple': + matches = np.trace(S, axis1=1, axis2=2) + columns = [matches, S.sum(axis=(1, 2)) - matches] + gaps + return np.column_stack(columns) if len(S) else np.empty((0, len(columns))) + # Rows and columns in the order of alphabet, whatever the order of the counts. + if alphabet != counts.alphabet: + order = [counts.alphabet.index(char) for char in alphabet] + S = S[:, order][:, :, order] + features = np.empty((len(S), len(alphabet)**2 + len(gaps))) + features[:, :len(alphabet)**2] = S.reshape(len(S), -1) + for k, column in enumerate(gaps, start=len(alphabet)**2): + features[:, k] = column + return features + + +def _fit_initial_estimate(predictors, labels, substitution_mode, gap_mode, alphabet): + """ + Logistic regression of the labels on the predictors. Simple mode is + unpenalized; the full matrix uses the default ridge penalty. + """ + if substitution_mode == 'simple': + logit = LogisticRegression(fit_intercept=True, penalty=None) + logit.fit(predictors, labels) + estimates = {'alpha': logit.intercept_[0], + 'match_score': logit.coef_[0][0], + 'mismatch_score': logit.coef_[0][1]} + else: + Asize = len(alphabet) + pair_to_id = {(char1, char2): Asize*i + j for i, char1 in enumerate(alphabet) for j, char2 in enumerate(alphabet)} + logit = LogisticRegression(fit_intercept=True, solver='newton-cg') + logit.fit(predictors, labels) + substitution_matrix = substitution_matrices.Array(data=np.zeros((Asize, Asize)), + alphabet=alphabet) + for char1 in alphabet: + for char2 in alphabet: + substitution_matrix[char1, char2] = logit.coef_[0][pair_to_id[(char1, char2)]] + if substitution_mode == 'symmetric': + # estimation of symmetric matrix should be implemented in + # a separate function, for now we use this trick + substitution_matrix = (substitution_matrix.T + substitution_matrix)/2 + estimates = {'alpha': logit.intercept_[0], + 'substitution_matrix': substitution_matrix} + if gap_mode == 'affine': + estimates['open_gap_score'] = logit.coef_[0][-2] + estimates['extend_gap_score'] = logit.coef_[0][-1] + else: + estimates['gap_score'] = logit.coef_[0][-1] return estimates +def _check_estimate_modes(substitution_mode, gap_mode, alphabet): + assert gap_mode in {'affine', 'linear'}, 'Only linear and affine gap modes are supported' + assert substitution_mode in {'simple', 'symmetric', 'general'} + if substitution_mode != 'simple': + assert alphabet is not None, 'General and symmetric substitution mode require to specify the alphabet' + + def get_initial_estimate(alignment_list, labels, substitution_mode = 'simple', gap_mode = 'affine', @@ -130,28 +175,22 @@ def get_initial_estimate(alignment_list, labels, using a simple logistic models with intercept and summary predictors: numbers of matches, mismatches, and gaps. """ - assert gap_mode in {'affine', 'linear'}, 'Only linear and affine gap modes are supported' - assert substitution_mode in {'simple', 'symmetric', 'general'} - if substitution_mode != 'simple': - assert alphabet is not None, 'General and symmetric substitution mode require to specify the alphabet' - if substitution_mode == 'simple': - if gap_mode == 'affine': - estimates = _initial_estim_affinegap_simplesubs(alignment_list, labels) - elif gap_mode == 'linear': - estimates = _initial_estim_lineargap_simplesubs(alignment_list, labels) - else: - estimates = _initial_estim_affinegap_fullsubs(alignment_list, labels, alphabet) - if substitution_mode == 'symmetric': - # estimation of symmetric matrix should be implemented in - # a separate function, for now we use this trick - subsM = estimates['substitution_matrix'] - subsM = (subsM.T + subsM)/2 - estimates['substitution_matrix'] = subsM - if gap_mode == 'linear': - estimates['gap_score'] = estimates['open_gap_score']+ estimates['extend_gap_score'] - del estimates['open_gap_score'] - del estimates['extend_gap_score'] - return estimates + _check_estimate_modes(substitution_mode, gap_mode, alphabet) + predictors = _alignment_features(alignment_list, substitution_mode, gap_mode, alphabet) + return _fit_initial_estimate(predictors, labels, substitution_mode, gap_mode, alphabet) + + +def get_initial_estimate_from_counts(counts, labels, + substitution_mode='simple', + gap_mode='affine', + alphabet=None): + """ + get_initial_estimate() from per-alignment counts instead of alignment + objects: CountArrays, or a list in logit_subgradient's format. + """ + _check_estimate_modes(substitution_mode, gap_mode, alphabet) + predictors = _count_features(counts, substitution_mode, gap_mode, alphabet) + return _fit_initial_estimate(predictors, labels, substitution_mode, gap_mode, alphabet) ### Parallel processing def create_alignment_workers(seqlistA, seqlistB, aligner): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..d2b4e0b --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,225 @@ +""" +Shared helpers for the test suite: random sequence data, aligners built from +DiscrimAlign parameter dicts, and an independent reference for the +log-likelihood as a function of the alignment parameters. +""" +import numpy as np +from Bio.Align import PairwiseAligner, substitution_matrices +from scipy.optimize import brentq +from scipy.special import expit + +from src.logit_link import logit_subgradient +from src.optimization import get_first_alignment + +DNA = "ACGT" + +MODES = ["local", "global"] +GAP_MODES = ["affine", "linear"] +SUBSTITUTION_MODES = ["simple", "symmetric", "general"] + + +def random_seq(rng, length, alphabet=DNA): + return "".join(rng.choice(list(alphabet), size=length)) + + +def mutate(rng, seq, sub_rate=0.1, indel_rate=0.05, alphabet=DNA): + """Point substitutions plus single-residue insertions and deletions.""" + out = [] + for char in seq: + u = rng.random() + if u < indel_rate / 2: + continue + if u < indel_rate: + out.append(rng.choice(list(alphabet))) + if rng.random() < sub_rate: + char = rng.choice([c for c in alphabet if c != char]) + out.append(char) + if not out: + out.append(seq[0]) + return "".join(out) + + +def make_pairs(rng, n_pos, n_neg, length, alphabet=DNA, + sub_rate=0.1, indel_rate=0.05): + """Positives are mutated copies, negatives are unrelated random sequences.""" + seqsA, seqsB, labels = [], [], [] + for _ in range(n_pos): + seq = random_seq(rng, length, alphabet) + seqsA.append(seq) + seqsB.append(mutate(rng, seq, sub_rate, indel_rate, alphabet)) + labels.append(1) + for _ in range(n_neg): + seqsA.append(random_seq(rng, length, alphabet)) + seqsB.append(random_seq(rng, length, alphabet)) + labels.append(0) + order = rng.permutation(len(labels)) + return ([seqsA[i] for i in order], + [seqsB[i] for i in order], + np.array([labels[i] for i in order])) + + +def random_params(rng, gap_mode, substitution_mode, alphabet=DNA, alpha=None): + """ + Random, non-integer parameters in DiscrimAlign's dict format. + + Continuous values make ties between alignments with different count + vectors a measure-zero event, so the log-likelihood is differentiable at + these points. The extend score is always above the open score. + """ + params = {"alpha": float(rng.normal(-1.0, 0.3)) if alpha is None else alpha} + if gap_mode == "affine": + params["open_gap_score"] = float(rng.uniform(-4.0, -2.5)) + params["extend_gap_score"] = float(rng.uniform(-1.5, -0.3)) + else: + params["gap_score"] = float(rng.uniform(-3.0, -1.0)) + if substitution_mode == "simple": + params["match_score"] = float(rng.uniform(1.5, 3.0)) + params["mismatch_score"] = float(rng.uniform(-2.0, -0.5)) + else: + n = len(alphabet) + data = rng.uniform(-2.0, -0.5, size=(n, n)) + data[np.diag_indices(n)] = rng.uniform(1.5, 3.0, size=n) + if substitution_mode == "symmetric": + data = (data + data.T) / 2 + params["substitution_matrix"] = substitution_matrices.Array( + alphabet=alphabet, data=data) + return params + + +def make_aligner(mode, params): + """A PairwiseAligner configured from a DiscrimAlign parameter dict.""" + aligner = PairwiseAligner() + aligner.mode = mode + if "gap_score" in params: + aligner.gap_score = params["gap_score"] + else: + aligner.open_gap_score = params["open_gap_score"] + aligner.extend_gap_score = params["extend_gap_score"] + if "substitution_matrix" in params: + aligner.substitution_matrix = params["substitution_matrix"] + else: + aligner.match_score = params["match_score"] + aligner.mismatch_score = params["mismatch_score"] + return aligner + + +def align_all(seqsA, seqsB, aligner): + return [get_first_alignment(a, b, aligner) for a, b in zip(seqsA, seqsB)] + + +def scores_at(seqsA, seqsB, mode, params): + aligner = make_aligner(mode, params) + return np.array([aln.score for aln in align_all(seqsA, seqsB, aligner)]) + + +def loglik_at(seqsA, seqsB, labels, mode, params): + """ + Log-likelihood at params, with alpha held fixed at params['alpha']. + + Computed from the logits as sum(y*z - log(1 + e^z)), independently of + logit_logL. + """ + z = params["alpha"] + scores_at(seqsA, seqsB, mode, params) + labels = np.asarray(labels, dtype=float) + return float(np.sum(labels * z - np.logaddexp(0.0, z))) + + +def optimal_alpha(scores, labels, bracket=(-200.0, 200.0)): + """Root of d logL / d alpha, by bracketing (independent of scipy.minimize).""" + scores = np.asarray(scores, dtype=float) + labels = np.asarray(labels, dtype=float) + return brentq(lambda a: np.sum(labels - expit(a + scores)), *bracket, xtol=1e-14) + + +def model_gradient(subgradient, gap_mode, substitution_mode): + """ + Map logit_subgradient's raw counts onto the model's free parameters, + in DiscrimAlign's dict format (without alpha). + """ + grad = {} + if gap_mode == "affine": + grad["open_gap_score"] = subgradient["Gap opens"] + grad["extend_gap_score"] = subgradient["Gap extends"] + else: + grad["gap_score"] = subgradient["Gap opens"] + subgradient["Gap extends"] + G = np.asarray(subgradient["Substitutions"]) + if substitution_mode == "simple": + grad["match_score"] = np.trace(G) + grad["mismatch_score"] = G.sum() - np.trace(G) + elif substitution_mode == "symmetric": + grad["substitution_matrix"] = G + G.T - np.diag(np.diag(G)) + else: + grad["substitution_matrix"] = G + return grad + + +def subgradient_at(seqsA, seqsB, labels, mode, params, alphabet): + aligner = make_aligner(mode, params) + alns = align_all(seqsA, seqsB, aligner) + scores = np.array([aln.score for aln in alns]) + logit_scores = expit(params["alpha"] + scores) + return logit_subgradient(alns, logit_scores, labels, params["alpha"], alphabet) + + +def perturbed(params, key, delta, index=None, symmetric=False): + """Copy of params with one scalar (or one matrix entry) shifted by delta.""" + new = dict(params) + if index is None: + new[key] = params[key] + delta + return new + M = substitution_matrices.Array(alphabet=params[key].alphabet, + data=np.array(params[key])) + i, j = index + M[i, j] += delta + if symmetric and i != j: + M[j, i] += delta + new[key] = M + return new + + +# Edge-case pairs mixed into every random fixture. +EDGE_PAIRS = [ + ("A", "A"), + ("A", "C"), + ("ACGT", "ACGT"), + ("AAAA", "CCCC"), + ("ACGTACGT", "ACG"), + ("G", "TTGTT"), +] + + +def small_fixture(seed, n=6, length=10): + """A few random pairs plus EDGE_PAIRS, with random labels.""" + rng = np.random.default_rng(seed) + seqsA, seqsB, labels = make_pairs(rng, n // 2, n - n // 2, length, sub_rate=0.2) + seqsA += [a for a, _ in EDGE_PAIRS] + seqsB += [b for _, b in EDGE_PAIRS] + labels = np.concatenate([labels, rng.integers(0, 2, size=len(EDGE_PAIRS))]) + return rng, seqsA, seqsB, labels + + +def default_baseline(mode, gap_mode, substitution_mode, alphabet=DNA, perturb=None): + """ + discrimalign()'s default baseline aligner. With perturb=rng, every score + is shifted by uniform noise in [-0.05, 0.05]: the integer defaults tie + often (two mismatches cost one gap open), and at a tie the backends may + pick different, equally optimal alignments with different counts. + """ + noise = (lambda: 0.0) if perturb is None else (lambda: float(perturb.uniform(-0.05, 0.05))) + aligner = PairwiseAligner() + aligner.mode = mode + if gap_mode == "affine": + aligner.open_gap_score = -8 + noise() + aligner.extend_gap_score = -0.5 + noise() + else: + aligner.gap_score = -6 + noise() + n = len(alphabet) + if substitution_mode == "simple": + aligner.match_score = 5 + noise() + aligner.mismatch_score = -4 + noise() + else: + data = 9 * np.eye(n) - 4 + if perturb is not None: + data = data + perturb.uniform(-0.05, 0.05, size=(n, n)) + aligner.substitution_matrix = substitution_matrices.Array(data=data, alphabet=alphabet) + return aligner diff --git a/tests/test_discrimalign.py b/tests/test_discrimalign.py new file mode 100644 index 0000000..4c3e501 --- /dev/null +++ b/tests/test_discrimalign.py @@ -0,0 +1,714 @@ +"""Integration tests for src.discrimalign.discrimalign on small fixtures.""" +import importlib +import sys +import warnings +from copy import deepcopy + +import numpy as np +import pytest +from Bio.Align import PairwiseAligner, substitution_matrices +from scipy.special import expit + +from src.discrimalign import _MAX_GAP_SCORE, discrimalign +from src.logit_link import logit_logL, logit_subgradient +from src.optimization import (EmptyLocalAlignment, create_constant_step, + create_powerstep, get_initial_estimate) +from tests.helpers import (DNA, GAP_MODES, MODES, SUBSTITUTION_MODES, align_all, + default_baseline, make_aligner, make_pairs, model_gradient, + optimal_alpha, random_params) + +ALL_MODES = [(m, g, s) for m in MODES for g in GAP_MODES for s in SUBSTITUTION_MODES] + + +def _data(seed=0, n=12, length=12): + rng = np.random.default_rng(seed) + seqsA, seqsB, labels = make_pairs(rng, n // 2, n - n // 2, length, sub_rate=0.25) + # Flip one label per class so the classes overlap and the intercept MLE exists. + labels[np.flatnonzero(labels == 1)[0]] = 0 + labels[np.flatnonzero(labels == 0)[0]] = 1 + return rng, seqsA, seqsB, labels + + +def _param_keys(gap_mode, substitution_mode): + keys = {"open_gap_score", "extend_gap_score"} if gap_mode == "affine" else {"gap_score"} + keys |= {"match_score", "mismatch_score"} if substitution_mode == "simple" else {"substitution_matrix"} + return keys + + +def _run(seqsA, seqsB, labels, mode, gap_mode, substitution_mode, **kwargs): + kwargs.setdefault("stepfunction", create_constant_step(0.01)) + kwargs.setdefault("max_iter", 2) + kwargs.setdefault("backend", "biopython") # tests that need nwgrad ask for it + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # sklearn convergence noise on tiny data + return discrimalign(seqsA, seqsB, labels, aligner_mode=mode, gap_mode=gap_mode, + substitution_mode=substitution_mode, **kwargs) + + +GAP_KEYS = ("open_gap_score", "extend_gap_score", "gap_score") + + +def _clip_gaps(params): + """The projection discrimalign applies: gap scores are capped.""" + for key in GAP_KEYS: + if key in params: + params[key] = np.minimum(params[key], _MAX_GAP_SCORE) + + +def _assert_alpha_stationary(alpha, scores, labels): + """dlogL/dalpha vanishes at alpha: the default alpha solver finds the exact root.""" + assert abs(np.sum(labels - expit(alpha + scores))) < 1e-9 + assert alpha == pytest.approx(optimal_alpha(scores, labels), abs=1e-9) + + +BACKENDS = ["biopython", "nwgrad"] + + +def _run_on(backend, *args, **kwargs): + return _run(*args, backend=backend, **kwargs) + + +def _assert_params_equal(result, expected, keys, **tol): + for key in keys: + np.testing.assert_allclose(np.asarray(result[key]), np.asarray(expected[key]), + err_msg=key, **tol) + + +# --- argument validation ---------------------------------------------------- + +@pytest.mark.parametrize("kwargs", [ + {"aligner_mode": "semiglobal"}, + {"gap_mode": "convex"}, + {"substitution_mode": "blosum"}, +]) +@pytest.mark.parametrize("backend", BACKENDS) +def test_rejects_unknown_modes(kwargs, backend): + _, A, B, y = _data() + with pytest.raises(AssertionError): + discrimalign(A, B, y, backend=backend, stepfunction=create_constant_step(0.1), max_iter=1, **kwargs) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_missing_stepfunction_defaults_to_powerstep(backend): + _, A, B, y = _data() + kwargs = dict(backend=backend, aligner_mode="local", gap_mode="affine", + substitution_mode="general", max_iter=3) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + default = discrimalign(A, B, y, **kwargs) + explicit = discrimalign(A, B, y, stepfunction=create_powerstep(1e-4), **kwargs) + for key in ("loglik_trajectory", "subgradient_l2_trajectory", "alpha", + "open_gap_score", "extend_gap_score"): + assert default[key] == explicit[key], key + np.testing.assert_array_equal(np.asarray(default["substitution_matrix"]), + np.asarray(explicit["substitution_matrix"])) + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("which", ["A", "B"]) +def test_empty_sequences_raise_before_any_work(which, backend, monkeypatch): + module = sys.modules["src.discrimalign"] + + def fail(*args, **kwargs): + raise AssertionError("aligned before validating the sequences") + + monkeypatch.setattr(module, "_align_pairs", fail) + _, A, B, y = _data() + if which == "A": + A[3] = "" + else: + B[1] = B[5] = "" + with pytest.raises(ValueError, match=f"seqlist{which} contains empty sequences"): + _run_on(backend, A, B, y, "local", "affine", "simple", max_iter=0, stepfunction=None) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_missing_stepfunction_is_fine_without_iterations(backend): + _, A, B, y = _data() + res = _run_on(backend, A, B, y, "local", "affine", "simple", max_iter=0, stepfunction=None) + assert res["loglik_trajectory"] == [res["final_loglik"]] + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_single_class_labels_without_initial_parameters_raise(backend): + _, A, B, _ = _data() + with pytest.raises(ValueError): + _run_on(backend, A, B, np.ones(len(A), dtype=int), "local", "affine", "simple") + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("value", [0, 1]) +def test_single_class_labels_raise_before_any_work(backend, value, monkeypatch): + """Checked once at the start: also with a warm start, which skips the initial + estimate, and before anything is aligned.""" + rng, A, B, _ = _data() + p0 = random_params(rng, "affine", "simple") + module = sys.modules["src.discrimalign"] + monkeypatch.setattr(module, "_align_pairs", lambda *a, **k: pytest.fail("aligned")) + with pytest.raises(ValueError, match="both classes"): + _run_on(backend, A, B, np.full(len(A), value), "local", "affine", "simple", + initial_parameters=p0) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_non_binary_labels_raise_before_any_work(backend, monkeypatch): + rng, A, B, y = _data() + p0 = random_params(rng, "affine", "simple") + y = np.array(y, dtype=float) + y[0] = 2 + module = sys.modules["src.discrimalign"] + monkeypatch.setattr(module, "_align_pairs", lambda *a, **k: pytest.fail("aligned")) + with pytest.raises(ValueError, match="0 or 1"): + _run_on(backend, A, B, y, "local", "affine", "simple", initial_parameters=p0) + + +# --- result structure ------------------------------------------------------- + +@pytest.mark.parametrize("backend", BACKENDS) +def test_inferred_alphabet_is_returned(backend): + _, A, B, y = _data() + res = _run_on(backend, A, B, y, "local", "affine", "simple", max_iter=1) + assert res["alphabet"] == "".join(sorted(set("".join(A) + "".join(B)))) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_given_alphabet_is_returned(backend): + _, A, B, y = _data() + res = _run_on(backend, A, B, y, "local", "affine", "general", max_iter=1, alphabet="TGCA") + assert res["alphabet"] == "TGCA" + assert "".join(res["substitution_matrix"].alphabet) == "TGCA" + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_result_structure(mode, gap_mode, substitution_mode, backend): + _, A, B, y = _data() + res = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, max_iter=3) + expected = _param_keys(gap_mode, substitution_mode) | { + "loglik_trajectory", "subgradient_l2_trajectory", "final_loglik", + "aligner", "alignments", "alignment_logit_scores", "alpha", "alphabet"} + assert set(res) == expected + assert len(res["loglik_trajectory"]) == 4 + assert len(res["subgradient_l2_trajectory"]) == 3 + assert res["final_loglik"] == res["loglik_trajectory"][-1] + assert len(res["alignments"]) == len(A) + assert len(res["alignment_logit_scores"]) == len(A) + assert np.all(np.asarray(res["subgradient_l2_trajectory"]) >= 0) + assert np.all(np.asarray(res["loglik_trajectory"]) <= 0) + assert res["aligner"].mode == mode + if substitution_mode != "simple": + assert "".join(res["substitution_matrix"].alphabet) == DNA + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_final_outputs_are_consistent(mode, gap_mode, substitution_mode, backend): + rng, A, B, y = _data(1) + p0 = random_params(rng, gap_mode, substitution_mode) + res = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0) + realigned = align_all(A, B, res["aligner"]) + scores = np.array([a.score for a in res["alignments"]]) + # nwgrad returns its own paths and scores, equal to Biopython's up to summation order. + tol = 0 if backend == "biopython" else 1e-12 + np.testing.assert_allclose(scores, [a.score for a in realigned], rtol=tol, atol=tol) + np.testing.assert_allclose(res["alignment_logit_scores"], expit(res["alpha"] + scores), rtol=1e-14) + assert res["final_loglik"] == pytest.approx(logit_logL(scores, res["alpha"], y), rel=1e-14) + # The returned aligner carries the returned parameters. + reference = make_aligner(mode, {k: res[k] for k in _param_keys(gap_mode, substitution_mode)}) + assert [a.score for a in realigned] == [a.score for a in align_all(A, B, reference)] + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_return_alignments_false_omits_alignments(backend): + _, A, B, y = _data() + res = _run_on(backend, A, B, y, "local", "affine", "simple", return_alignments=False) + assert "alignments" not in res + assert len(res["alignment_logit_scores"]) == len(A) + + +# --- zero iterations and zero steps ----------------------------------------- + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_zero_iterations_return_initial_parameters(mode, gap_mode, substitution_mode, backend): + rng, A, B, y = _data(2) + p0 = random_params(rng, gap_mode, substitution_mode) + res = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0, + max_iter=0, stepfunction=None) + _assert_params_equal(res, p0, _param_keys(gap_mode, substitution_mode)) + assert res["alpha"] == p0["alpha"] + assert res["subgradient_l2_trajectory"] == [] + scores = np.array([a.score for a in align_all(A, B, make_aligner(mode, p0))]) + assert res["final_loglik"] == pytest.approx(logit_logL(scores, p0["alpha"], y)) + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_zero_step_keeps_parameters_and_fits_alpha(mode, gap_mode, substitution_mode, backend): + rng, A, B, y = _data(3) + p0 = random_params(rng, gap_mode, substitution_mode) + res = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0, + max_iter=3, stepfunction=create_constant_step(0.0)) + _assert_params_equal(res, p0, _param_keys(gap_mode, substitution_mode)) + scores = np.array([a.score for a in res["alignments"]]) + _assert_alpha_stationary(res["alpha"], scores, y) + traj = res["loglik_trajectory"] + assert traj[1] >= traj[0] - 1e-12 + assert traj[1] == pytest.approx(traj[2], abs=1e-8) + assert traj[2] == pytest.approx(traj[3], abs=1e-8) + + +# --- one iteration is exactly one subgradient step -------------------------- + +@pytest.mark.parametrize("backend", ["biopython", "nwgrad"]) +@pytest.mark.parametrize("seed", [4, 5]) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_one_iteration_is_one_subgradient_step(mode, gap_mode, substitution_mode, seed, backend): + rng, A, B, y = _data(seed) + p0 = random_params(rng, gap_mode, substitution_mode) + eta = 0.013 + res = _run(A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0, + max_iter=1, stepfunction=create_constant_step(eta), backend=backend) + + alns = align_all(A, B, make_aligner(mode, p0)) + scores = np.array([a.score for a in alns]) + alpha = res["alpha"] + # alpha is the intercept MLE for the scores under p0 ... + _assert_alpha_stationary(alpha, scores, y) + # ... and the step is taken with the gradient at that alpha. + logits = expit(alpha + scores) + grad = model_gradient(logit_subgradient(alns, logits, y, alpha, DNA), gap_mode, substitution_mode) + expected = {k: np.asarray(p0[k]) + eta * np.asarray(grad[k]) for k in grad} + _clip_gaps(expected) + _assert_params_equal(res, expected, grad.keys(), rtol=1e-12, atol=1e-12) + + norm = np.sqrt(sum(np.sum(np.asarray(g) ** 2) for g in grad.values())) + assert res["subgradient_l2_trajectory"][0] == pytest.approx(norm, rel=1e-12) + assert res["loglik_trajectory"][0] == pytest.approx(logit_logL(scores, p0["alpha"], y)) + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode", MODES) +def test_small_step_increases_likelihood_at_fixed_alpha(mode, backend): + """A short step along the subgradient is an ascent direction.""" + rng, A, B, y = _data(6, n=16) + p0 = random_params(rng, "affine", "general") + res = _run_on(backend, A, B, y, mode, "affine", "general", initial_parameters=p0, + max_iter=1, stepfunction=create_constant_step(1e-4)) + p0_fitted = dict(p0, alpha=res["alpha"]) + p1 = {k: res[k] for k in _param_keys("affine", "general")} + p1["alpha"] = res["alpha"] + + def loglik(p): + s = np.array([a.score for a in align_all(A, B, make_aligner(mode, p))]) + return logit_logL(s, p["alpha"], y) + + assert loglik(p1) > loglik(p0_fitted) + + +# --- invariants over several iterations ------------------------------------- + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode", [(m, g) for m in MODES for g in GAP_MODES]) +def test_symmetric_mode_keeps_matrix_symmetric(mode, gap_mode, backend): + _, A, B, y = _data(7) + res = _run_on(backend, A, B, y, mode, gap_mode, "symmetric", max_iter=4, + stepfunction=create_powerstep(0.05)) + M = np.asarray(res["substitution_matrix"]) + np.testing.assert_allclose(M, M.T, atol=1e-12) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_general_mode_can_become_asymmetric(backend): + rng, A, B, y = _data(8) + p0 = random_params(rng, "affine", "symmetric") + res = _run_on(backend, A, B, y, "global", "affine", "general", initial_parameters=p0, + max_iter=2, stepfunction=create_constant_step(0.05)) + M = np.asarray(res["substitution_matrix"]) + assert not np.allclose(M, M.T) + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_inputs_are_not_mutated(mode, gap_mode, substitution_mode, backend): + rng, A, B, y = _data(9) + p0 = random_params(rng, gap_mode, substitution_mode) + baseline = make_aligner(mode, random_params(rng, gap_mode, substitution_mode)) + snapshot = (deepcopy(p0), list(A), list(B), y.copy(), + [a.score for a in align_all(A, B, baseline)]) + _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0, + baseline_aligner=baseline, max_iter=2, stepfunction=create_constant_step(0.1)) + _assert_params_equal(p0, snapshot[0], p0.keys()) + assert (A, B) == (snapshot[1], snapshot[2]) + np.testing.assert_array_equal(y, snapshot[3]) + assert [a.score for a in align_all(A, B, baseline)] == snapshot[4] + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", + [("local", "affine", "symmetric"), ("global", "linear", "simple")]) +def test_thread_count_does_not_change_results(mode, gap_mode, substitution_mode, backend): + _, A, B, y = _data(10, n=20) + one = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, max_iter=3, num_threads=1) + four = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, max_iter=3, num_threads=4) + _assert_params_equal(four, one, _param_keys(gap_mode, substitution_mode), rtol=0, atol=0) + assert four["loglik_trajectory"] == one["loglik_trajectory"] + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("gap_mode, substitution_mode", + [("affine", "simple"), ("linear", "general"), ("affine", "symmetric")]) +def test_subgradient_scale_is_equivalent_to_scaled_step(gap_mode, substitution_mode, backend): + rng, A, B, y = _data(11) + p0 = random_params(rng, gap_mode, substitution_mode) + scaled = _run_on(backend, A, B, y, "local", gap_mode, substitution_mode, initial_parameters=p0, + max_iter=3, subgradient_scale=0.5, stepfunction=create_constant_step(0.02)) + plain = _run_on(backend, A, B, y, "local", gap_mode, substitution_mode, initial_parameters=p0, + max_iter=3, stepfunction=create_constant_step(0.01)) + _assert_params_equal(scaled, plain, _param_keys(gap_mode, substitution_mode), rtol=1e-12) + np.testing.assert_allclose(scaled["subgradient_l2_trajectory"], + 0.5 * np.asarray(plain["subgradient_l2_trajectory"]), rtol=1e-12) + + +# --- stochastic_factor ------------------------------------------------------ + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +def test_stochastic_factor_is_reproducible_under_global_seed(substitution_mode, backend): + rng, A, B, y = _data(12) + p0 = random_params(rng, "affine", substitution_mode) + runs = [] + for _ in range(2): + np.random.seed(123) + runs.append(_run_on(backend, A, B, y, "local", "affine", substitution_mode, initial_parameters=p0, + stochastic_factor=0.5, max_iter=2)) + keys = _param_keys("affine", substitution_mode) + _assert_params_equal(runs[0], runs[1], keys, rtol=0, atol=0) + quiet = _run_on(backend, A, B, y, "local", "affine", substitution_mode, initial_parameters=p0, max_iter=2) + assert any(not np.allclose(np.asarray(runs[0][k]), np.asarray(quiet[k])) for k in keys) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_symmetric_mode_stays_symmetric_with_noise(backend): + rng, A, B, y = _data(13) + np.random.seed(7) + res = _run_on(backend, A, B, y, "global", "affine", "symmetric", + initial_parameters=random_params(rng, "affine", "symmetric"), + stochastic_factor=1.0, max_iter=3) + M = np.asarray(res["substitution_matrix"]) + np.testing.assert_allclose(M, M.T, atol=1e-12) + + +# --- alphabet handling ------------------------------------------------------ + +@pytest.mark.parametrize("backend", BACKENDS) +def test_alphabet_inferred_from_sequences_is_sorted_union(backend): + A = ["GATTACA", "CAT", "TAG", "GGT"] + B = ["GATACA", "CAT", "ACG", "TTT"] + res = _run_on(backend, A, B, np.array([1, 1, 0, 0]), "local", "affine", "general", max_iter=1) + assert "".join(res["substitution_matrix"].alphabet) == "ACGT" + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_alphabet_taken_from_initial_parameters(backend): + rng, A, B, y = _data(14) + p0 = random_params(rng, "affine", "general", alphabet="TGCA") + res = _run_on(backend, A, B, y, "local", "affine", "general", initial_parameters=p0, max_iter=1) + assert "".join(res["substitution_matrix"].alphabet) == "TGCA" + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_alphabet_taken_from_baseline_aligner(backend): + rng, A, B, y = _data(15) + baseline = make_aligner("local", random_params(rng, "affine", "general", alphabet="CATG")) + res = _run_on(backend, A, B, y, "local", "affine", "general", baseline_aligner=baseline, max_iter=1) + assert "".join(res["substitution_matrix"].alphabet) == "CATG" + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_explicit_alphabet_wins(backend): + _, A, B, y = _data(16) + res = _run_on(backend, A, B, y, "local", "affine", "general", alphabet="GTAC", max_iter=1) + assert "".join(res["substitution_matrix"].alphabet) == "GTAC" + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_explicit_alphabet_missing_a_letter_fails(backend): + _, A, B, y = _data(17) + with pytest.raises((KeyError, IndexError, ValueError)): + _run_on(backend, A, B, y, "local", "affine", "general", alphabet="ACG", max_iter=1) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_protein_alphabet(backend): + rng = np.random.default_rng(18) + protein = "ACDEFGHIKLMNPQRSTVWY" + A, B, y = make_pairs(rng, 5, 5, 15, alphabet=protein, sub_rate=0.2) + res = _run_on(backend, A, B, y, "local", "affine", "symmetric", alphabet=protein, max_iter=2) + assert np.asarray(res["substitution_matrix"]).shape == (20, 20) + + +# --- baselines used for the initial estimate -------------------------------- + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_initial_estimate_uses_default_baseline(mode, gap_mode, substitution_mode): + _, A, B, y = _data(19, n=20) + res = _run(A, B, y, mode, gap_mode, substitution_mode, max_iter=0, stepfunction=None) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + expected = get_initial_estimate( + align_all(A, B, default_baseline(mode, gap_mode, substitution_mode)), y, + substitution_mode=substitution_mode, gap_mode=gap_mode, alphabet=DNA) + _clip_gaps(expected) + _assert_params_equal(res, expected, _param_keys(gap_mode, substitution_mode), rtol=1e-12) + assert res["alpha"] == pytest.approx(expected["alpha"], rel=1e-12) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_initial_estimate_uses_given_baseline_aligner(backend): + rng, A, B, y = _data(20, n=20) + baseline = make_aligner("global", random_params(rng, "linear", "simple")) + res = _run_on(backend, A, B, y, "global", "linear", "simple", baseline_aligner=baseline, + max_iter=0, stepfunction=None) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + expected = get_initial_estimate(align_all(A, B, baseline), y, "simple", "linear") + _clip_gaps(expected) + _assert_params_equal(res, expected, {"match_score", "mismatch_score", "gap_score"}, rtol=1e-12) + + +# --- data edge cases -------------------------------------------------------- + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("gap_mode, substitution_mode", [("affine", "simple"), ("linear", "general")]) +def test_local_pairs_without_alignment(gap_mode, substitution_mode, backend): + A = ["AAAA", "ACGT", "CCCC", "ACGTAC", "GGGG", "ACGA"] + B = ["CCCC", "ACGT", "GGGG", "ACGTAC", "TTTT", "ACGT"] + y = np.array([0, 1, 0, 1, 0, 1]) + rng = np.random.default_rng(21) + p0 = random_params(rng, gap_mode, substitution_mode) + res = _run_on(backend, A, B, y, "local", gap_mode, substitution_mode, initial_parameters=p0, + max_iter=2) + for i in (0, 2, 4): + assert isinstance(res["alignments"][i], EmptyLocalAlignment) + assert res["alignment_logit_scores"][i] == pytest.approx(expit(res["alpha"])) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_single_residue_sequences(backend): + A = ["A", "C", "G", "T", "A", "C"] + B = ["A", "C", "G", "A", "C", "T"] + y = np.array([1, 1, 1, 0, 0, 0]) + rng = np.random.default_rng(22) + res = _run_on(backend, A, B, y, "global", "affine", "general", + initial_parameters=random_params(rng, "affine", "general"), max_iter=3) + assert np.all(np.isfinite(np.asarray(res["substitution_matrix"]))) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_list_labels_behave_like_array_labels(backend): + rng, A, B, y = _data(23) + p0 = random_params(rng, "affine", "simple") + as_array = _run_on(backend, A, B, y, "local", "affine", "simple", initial_parameters=p0, max_iter=2) + as_list = _run_on(backend, A, B, list(y), "local", "affine", "simple", initial_parameters=p0, max_iter=2) + _assert_params_equal(as_list, as_array, _param_keys("affine", "simple"), rtol=0, atol=0) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_verbose_prints_progress(capsys, backend): + rng, A, B, y = _data(24) + _run_on(backend, A, B, y, "local", "affine", "simple", + initial_parameters=random_params(rng, "affine", "simple"), max_iter=1, verbose=True) + out = capsys.readouterr().out + assert "Alphabet:" in out + assert "Start of iteration 0" in out + assert "End of iteration 0" in out + + +# --- gap scores are projected onto <= 0 ------------------------------------- + +@pytest.mark.parametrize("backend", ["biopython", "nwgrad"]) +@pytest.mark.parametrize("gap_mode, positive", [ + ("affine", {"open_gap_score": 0.4}), + ("affine", {"extend_gap_score": 0.2}), + ("affine", {"open_gap_score": 0.4, "extend_gap_score": 0.2}), + ("linear", {"gap_score": 0.3}), +]) +def test_positive_starting_gap_scores_are_projected(gap_mode, positive, backend): + rng, A, B, y = _data(25) + p0 = random_params(rng, gap_mode, "simple") + negative = {k: p0[k] for k in GAP_KEYS if k in p0 and k not in positive} + p0.update(positive) + res = _run(A, B, y, "local", gap_mode, "simple", initial_parameters=p0, + max_iter=0, stepfunction=None, backend=backend) + for key in positive: + assert res[key] == _MAX_GAP_SCORE + for key, value in negative.items(): + assert res[key] == value + assert p0[next(iter(positive))] > 0 # the caller's dict is not modified + + +@pytest.mark.parametrize("backend", ["biopython", "nwgrad"]) +@pytest.mark.parametrize("mode, gap_mode", [(m, g) for m in MODES for g in GAP_MODES]) +def test_gap_scores_never_become_positive(mode, gap_mode, backend): + """Steps large enough to overshoot the cap are projected back onto it.""" + hits_cap = False + for seed in range(4): + rng, A, B, y = _data(26 + seed) + p0 = random_params(rng, gap_mode, "simple") + for key in GAP_KEYS: + if key in p0: + p0[key] = 2 * _MAX_GAP_SCORE + res = _run(A, B, y, mode, gap_mode, "simple", initial_parameters=p0, max_iter=3, + stepfunction=create_constant_step(1.0), backend=backend) + gaps = [res[k] for k in GAP_KEYS if k in res] + assert all(g <= _MAX_GAP_SCORE for g in gaps) + hits_cap |= any(g == _MAX_GAP_SCORE for g in gaps) + assert hits_cap + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_substitution_scores_are_not_projected(backend): + rng, A, B, y = _data(30) + p0 = random_params(rng, "affine", "simple") + res = _run_on(backend, A, B, y, "local", "affine", "simple", initial_parameters=p0, max_iter=2) + assert res["match_score"] > 0 + + +def test_discrimalign_uses_default_stepfunction(): + result = discrimalign( + seqlistA=["AUGCUA", "CUGA"], + seqlistB=["AUGGUA", "CUGU"], + labels=[1, 0], + aligner_mode="local", + gap_mode="affine", + substitution_mode="symmetric", + max_iter=1, + num_threads=1, + ) + + assert "final_loglik" in result + assert "alpha" in result + + +# --- alpha_solver ------------------------------------------------------------- + +def _record_alpha_fits(monkeypatch): + """ + Record (scores, labels, alpha) for every alpha fit: discrimalign's Python + _fit_alpha_unchecked, and nwgrad.logistic.step where the nwgrad backend uses it. + """ + module = sys.modules["src.logit_link"] + calls = [] + original = module._fit_alpha_unchecked + + def recording(scores, labels, alpha0, **kwargs): + alpha = original(scores, labels, alpha0, **kwargs) + calls.append((np.array(scores, dtype=float), np.array(labels, dtype=float), alpha)) + return alpha + + monkeypatch.setattr(module, "_fit_alpha_unchecked", recording) + + backend = importlib.import_module("src.nwgrad_backend") + logistic = backend.nwgrad_logistic + + class RecordingLogistic: + @staticmethod + def step(batch, labels, alpha0): + st = logistic.step(batch, labels, alpha0) + calls.append((np.array(batch.scores()), np.array(labels, dtype=float), st.alpha)) + return st + + monkeypatch.setattr(backend, "nwgrad_logistic", RecordingLogistic) + return calls + + +def test_default_alpha_solver_is_safeguarded_newton(): + import inspect + default = inspect.signature(discrimalign).parameters["alpha_solver"].default + assert default == "safeguarded_newton" + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", + [("local", "affine", "general"), ("global", "linear", "simple")]) +def test_alpha_is_the_exact_optimum_at_every_iteration(backend, mode, gap_mode, + substitution_mode, monkeypatch): + calls = _record_alpha_fits(monkeypatch) + _, A, B, y = _data(20) + res = _run_on(backend, A, B, y, mode, gap_mode, substitution_mode, max_iter=4) + assert len(calls) == 4 + for scores, labels, alpha in calls: + assert alpha == pytest.approx(optimal_alpha(scores, labels), abs=1e-10) + assert res["alpha"] == calls[-1][2] + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_alpha_stays_exact_when_the_optimum_jumps_far(backend, monkeypatch): + """A large summed-gradient step moves every score far, so alpha* jumps between iterations.""" + calls = _record_alpha_fits(monkeypatch) + _, A, B, y = _data(21) + _run_on(backend, A, B, y, "local", "affine", "general", max_iter=4, + stepfunction=create_constant_step(5.0)) + jumps = [abs(b[2] - a[2]) for a, b in zip(calls, calls[1:])] + assert max(jumps) > 10 + for scores, labels, alpha in calls: + ref = optimal_alpha(scores, labels, bracket=(alpha - 100.0, alpha + 100.0)) + assert alpha == pytest.approx(ref, abs=1e-9) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_bfgs_alpha_solver_reaches_the_same_fit(backend): + _, A, B, y = _data(22) + newton = _run_on(backend, A, B, y, "local", "affine", "general", max_iter=3) + bfgs = _run_on(backend, A, B, y, "local", "affine", "general", max_iter=3, + alpha_solver="bfgs") + assert bfgs["alpha"] == pytest.approx(newton["alpha"], abs=1e-4) + _assert_params_equal(bfgs, newton, _param_keys("affine", "general"), atol=1e-4) + + +def test_unknown_alpha_solver_is_rejected(): + _, A, B, y = _data(23) + with pytest.raises(AssertionError): + discrimalign(A, B, y, max_iter=0, alpha_solver="newton-cg") + + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_nwgrad_logistic_step_matches_the_python_path(mode, gap_mode, substitution_mode, monkeypatch): + """nwgrad.logistic.step and the numpy path (the Biopython backend's) give the same fit.""" + backend = importlib.import_module("src.nwgrad_backend") + _, A, B, y = _data(24) + kwargs = dict(max_iter=5, stepfunction=create_constant_step(0.01)) + native = _run_on("nwgrad", A, B, y, mode, gap_mode, substitution_mode, **kwargs) + # The numpy path (logit_link.logistic_step) on the nwgrad engine. + monkeypatch.setattr(backend.NwgradEngine, "logistic_step", + lambda self, labels, alpha0, alpha_solver: + backend.logistic_step(self, labels, alpha0, alpha_solver)) + python = _run_on("nwgrad", A, B, y, mode, gap_mode, substitution_mode, **kwargs) + assert native["alpha"] == pytest.approx(python["alpha"], rel=1e-12, abs=1e-12) + np.testing.assert_allclose(native["loglik_trajectory"], python["loglik_trajectory"], rtol=1e-12) + _assert_params_equal(native, python, _param_keys(gap_mode, substitution_mode), + rtol=1e-12, atol=1e-12) + + +@pytest.mark.parametrize("fill", ["rowwise", "interpair"]) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_fill_gives_the_same_fit(mode, gap_mode, substitution_mode, fill): + """nwgrad_fill changes only the speed: the fit is bit-identical.""" + _, A, B, y = _data(25) + kwargs = dict(max_iter=5, stepfunction=create_constant_step(0.01)) + striped = _run_on("nwgrad", A, B, y, mode, gap_mode, substitution_mode, **kwargs) + other = _run_on("nwgrad", A, B, y, mode, gap_mode, substitution_mode, + nwgrad_fill=fill, **kwargs) + assert other["alpha"] == striped["alpha"] + assert other["loglik_trajectory"] == striped["loglik_trajectory"] + _assert_params_equal(other, striped, _param_keys(gap_mode, substitution_mode), + rtol=0, atol=0) + + +def test_nwgrad_fill_is_validated(): + _, A, B, y = _data(26) + with pytest.raises(ValueError, match="nwgrad_fill must be"): + discrimalign(A, B, y, max_iter=0, backend="nwgrad", nwgrad_fill="diagonal") + with pytest.raises(ValueError, match="nwgrad backend only"): + discrimalign(A, B, y, max_iter=0, backend="biopython", nwgrad_fill="rowwise") diff --git a/tests/test_discrimalign_helpers.py b/tests/test_discrimalign_helpers.py new file mode 100644 index 0000000..5555a56 --- /dev/null +++ b/tests/test_discrimalign_helpers.py @@ -0,0 +1,135 @@ +"""Unit tests for the private helpers in src.discrimalign.""" +import numpy as np +import pytest +from Bio.Align import PairwiseAligner, substitution_matrices + +from src.discrimalign import (_align_pair_chunk, _align_pairs, _matrix_alphabet, + _pair_chunks, _resolve_alphabet, _warm_start_alphabet) +from src.optimization import EmptyLocalAlignment +from tests.helpers import make_aligner, make_pairs, random_params + + +# --- _pair_chunks ----------------------------------------------------------- + +@pytest.mark.parametrize("n, chunk_size", [(0, 3), (1, 1), (5, 1), (5, 2), (6, 3), (7, 10)]) +def test_pair_chunks_cover_input_in_order(n, chunk_size): + A = [f"a{i}" for i in range(n)] + B = [f"b{i}" for i in range(n)] + chunks = list(_pair_chunks(A, B, chunk_size)) + assert [p for chunk in chunks for p in chunk] == list(zip(A, B)) + assert all(1 <= len(c) <= chunk_size for c in chunks) + assert len(chunks) == -(-n // chunk_size) + + +# --- _align_pair_chunk / _align_pairs --------------------------------------- + +def _aligner(mode): + rng = np.random.default_rng(0) + return make_aligner(mode, random_params(rng, "affine", "simple")) + + +@pytest.mark.parametrize("mode", ["local", "global"]) +def test_align_pair_chunk_matches_direct_alignment(mode): + aligner = _aligner(mode) + pairs = [("ACGT", "ACT"), ("GGGA", "GGA"), ("T", "T")] + alns = _align_pair_chunk(pairs, aligner) + assert [a.score for a in alns] == pytest.approx( + [aligner.score(x, y) for x, y in pairs], rel=1e-12) + + +def test_align_pair_chunk_returns_empty_local_alignment(): + aligner = _aligner("local") + alns = _align_pair_chunk([("AAA", "CCC")], aligner) + assert isinstance(alns[0], EmptyLocalAlignment) + + +@pytest.mark.parametrize("num_threads", [1, 2, 3, 8, 64]) +@pytest.mark.parametrize("mode", ["local", "global"]) +def test_align_pairs_is_independent_of_thread_count(num_threads, mode): + rng = np.random.default_rng(num_threads) + seqsA, seqsB, _ = make_pairs(rng, 9, 8, 15) + aligner = _aligner(mode) + alns = _align_pairs(seqsA, seqsB, aligner, num_threads) + serial = _align_pairs(seqsA, seqsB, aligner, 1) + assert len(alns) == len(seqsA) + assert [a.score for a in alns] == [a.score for a in serial] + # aligner.score() sums in a different order than align(), so only approx. + assert [a.score for a in alns] == pytest.approx( + [aligner.score(a, b) for a, b in zip(seqsA, seqsB)], rel=1e-12) + + +@pytest.mark.parametrize("num_threads", [1, 4]) +def test_align_pairs_empty_input(num_threads): + assert _align_pairs([], [], _aligner("global"), num_threads) == [] + + +def test_align_pairs_preserves_order_with_distinct_scores(): + aligner = _aligner("global") + seqsA = ["A" * k for k in range(1, 21)] + seqsB = ["A" * k for k in range(1, 21)] + scores = [a.score for a in _align_pairs(seqsA, seqsB, aligner, 4)] + assert scores == sorted(scores) + assert len(set(scores)) == 20 + + +# --- _matrix_alphabet / _warm_start_alphabet -------------------------------- + +def _matrix(alphabet): + n = len(alphabet) + return substitution_matrices.Array(alphabet=alphabet, data=np.eye(n)) + + +def test_matrix_alphabet_of_array(): + assert _matrix_alphabet(_matrix("TGCA")) == "TGCA" + + +@pytest.mark.parametrize("matrix", [None, np.eye(2), 3.0]) +def test_matrix_alphabet_without_alphabet_attribute(matrix): + assert _matrix_alphabet(matrix) is None + + +def test_warm_start_alphabet_from_initial_parameters(): + assert _warm_start_alphabet(None, {"substitution_matrix": _matrix("CGAT")}) == "CGAT" + + +def test_warm_start_alphabet_from_baseline_aligner(): + aligner = PairwiseAligner() + aligner.substitution_matrix = _matrix("TCAG") + assert _warm_start_alphabet(aligner, None) == "TCAG" + + +def test_warm_start_alphabet_prefers_initial_parameters(): + aligner = PairwiseAligner() + aligner.substitution_matrix = _matrix("TCAG") + assert _warm_start_alphabet(aligner, {"substitution_matrix": _matrix("GATC")}) == "GATC" + + +def test_warm_start_alphabet_falls_back_to_aligner_without_matrix_in_params(): + aligner = PairwiseAligner() + aligner.substitution_matrix = _matrix("TCAG") + assert _warm_start_alphabet(aligner, {"match_score": 1.0}) == "TCAG" + + +def test_warm_start_alphabet_none_for_simple_scoring(): + aligner = PairwiseAligner() + aligner.match_score = 2 + assert _warm_start_alphabet(aligner, {"match_score": 2.0}) is None + assert _warm_start_alphabet(None, None) is None + + +# --- _resolve_alphabet ------------------------------------------------------- + +def test_resolve_alphabet_given_wins(): + aligner = PairwiseAligner() + aligner.substitution_matrix = _matrix("TCAG") + assert _resolve_alphabet("ACGT", ["XY"], ["Z"], aligner, + {"substitution_matrix": _matrix("GATC")}) == "ACGT" + + +def test_resolve_alphabet_from_warm_start(): + assert _resolve_alphabet(None, ["XY"], ["Z"], None, + {"substitution_matrix": _matrix("GATC")}) == "GATC" + + +def test_resolve_alphabet_from_sequences_is_sorted_union(): + assert _resolve_alphabet(None, ["GAT", "TTA"], ["CAN"], None, {"match_score": 1.0}) == "ACGNT" diff --git a/tests/test_gradient_consistency.py b/tests/test_gradient_consistency.py new file mode 100644 index 0000000..ed0b238 --- /dev/null +++ b/tests/test_gradient_consistency.py @@ -0,0 +1,224 @@ +""" +Integration tests tying the subgradient to the alignment scores. + +For a hard (Viterbi) alignment, the score is linear in the parameters with +the path's count vector as coefficients, and the log-likelihood gradient is +sum_i (y_i - p_i) * counts_i. These tests check both facts against +Biopython's scores, at random non-integer parameters where the optimal path +is locally unique. +""" +import itertools + +import numpy as np +import pytest +from Bio.Align import PairwiseAligner, substitution_matrices + +from src.logit_link import logit_subgradient +from tests.helpers import (DNA, GAP_MODES, MODES, SUBSTITUTION_MODES, align_all, + small_fixture, + loglik_at, make_aligner, make_pairs, model_gradient, + perturbed, random_params, subgradient_at) + +SEEDS = range(5) + +def _counts_dot_params(sg, params): + """Score implied by a single alignment's counts (weight 1).""" + G = np.asarray(sg["Substitutions"]) + if "substitution_matrix" in params: + score = np.sum(G * np.asarray(params["substitution_matrix"])) + else: + score = np.trace(G) * params["match_score"] + (G.sum() - np.trace(G)) * params["mismatch_score"] + if "gap_score" in params: + score += (sg["Gap opens"] + sg["Gap extends"]) * params["gap_score"] + else: + score += sg["Gap opens"] * params["open_gap_score"] + sg["Gap extends"] * params["extend_gap_score"] + return score + + +# --- score = counts . parameters -------------------------------------------- + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("mode", MODES) +def test_score_equals_counts_dot_parameters(mode, gap_mode, substitution_mode, seed): + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, gap_mode, substitution_mode) + for aln in align_all(seqsA, seqsB, make_aligner(mode, params)): + sg = logit_subgradient([aln], [0.0], [1], 0.0, DNA) + assert _counts_dot_params(sg, params) == pytest.approx(aln.score, rel=1e-12, abs=1e-12) + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("mode", MODES) +def test_biopython_counts_match_subgradient_counts(mode, seed): + """The initial estimate's features (aln.counts()) agree with logit_subgradient.""" + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, "affine", "simple") + for aln in align_all(seqsA, seqsB, make_aligner(mode, params)): + sg = logit_subgradient([aln], [0.0], [1], 0.0, DNA) + G = np.asarray(sg["Substitutions"]) + counts = aln.counts() + assert counts.identities == np.trace(G) + assert counts.mismatches == G.sum() - np.trace(G) + assert counts.open_gaps == sg["Gap opens"] + assert counts.extend_gaps == sg["Gap extends"] + assert counts.gaps == sg["Gap opens"] + sg["Gap extends"] + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("mode", MODES) +def test_biopython_counts_reproduce_score(mode, seed): + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, "affine", "simple") + for aln in align_all(seqsA, seqsB, make_aligner(mode, params)): + c = aln.counts() + implied = (c.identities * params["match_score"] + c.mismatches * params["mismatch_score"] + + c.open_gaps * params["open_gap_score"] + c.extend_gaps * params["extend_gap_score"]) + assert implied == pytest.approx(aln.score, rel=1e-12, abs=1e-12) + + +def test_local_score_is_nonnegative_and_global_is_not_bounded(): + rng = np.random.default_rng(0) + params = random_params(rng, "affine", "simple") + local = make_aligner("local", params) + glob = make_aligner("global", params) + for a, b in [("AAAA", "CCCC"), ("ACAC", "GTGT"), ("A", "G")]: + assert align_all([a], [b], local)[0].score == 0.0 + assert align_all([a], [b], glob)[0].score < 0.0 + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +def test_local_score_dominates_global_score(gap_mode, substitution_mode, seed): + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, gap_mode, substitution_mode) + local = align_all(seqsA, seqsB, make_aligner("local", params)) + glob = align_all(seqsA, seqsB, make_aligner("global", params)) + for l, g in zip(local, glob): + assert l.score >= g.score - 1e-12 + + +def test_score_decomposition_with_alternating_gaps(): + """With extend below open, Biopython returns -A-B / C-D-: four opens, no extends.""" + params = {"match_score": 1.0, "mismatch_score": -100.0, + "open_gap_score": -1.0, "extend_gap_score": -10.0} + aln = align_all(["AB"], ["CD"], make_aligner("global", params))[0] + sg = logit_subgradient([aln], [0.0], [1], 0.0, "ABCD") + assert _counts_dot_params(sg, params) == pytest.approx(aln.score) + + +# --- subgradient = d logL / d theta (central differences) ------------------- + +def _directions(params, substitution_mode, alphabet=DNA): + """(key, index, symmetric) for every free parameter of the model.""" + for key in ("open_gap_score", "extend_gap_score", "gap_score", + "match_score", "mismatch_score"): + if key in params: + yield key, None, False + if "substitution_matrix" in params: + n = len(alphabet) + if substitution_mode == "symmetric": + pairs = [(i, j) for i in range(n) for j in range(i, n)] + else: + pairs = list(itertools.product(range(n), repeat=2)) + for i, j in pairs: + yield "substitution_matrix", (i, j), substitution_mode == "symmetric" + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("mode", MODES) +def test_subgradient_matches_finite_differences(mode, gap_mode, substitution_mode, seed): + rng, seqsA, seqsB, labels = small_fixture(seed) + params = random_params(rng, gap_mode, substitution_mode) + sg = subgradient_at(seqsA, seqsB, labels, mode, params, DNA) + grad = model_gradient(sg, gap_mode, substitution_mode) + h = 1e-6 + for key, index, symmetric in _directions(params, substitution_mode): + up = loglik_at(seqsA, seqsB, labels, mode, perturbed(params, key, h, index, symmetric)) + down = loglik_at(seqsA, seqsB, labels, mode, perturbed(params, key, -h, index, symmetric)) + numeric = (up - down) / (2 * h) + analytic = grad[key] if index is None else grad[key][index] + assert analytic == pytest.approx(numeric, rel=1e-5, abs=1e-6), (key, index) + + +def _nwgrad_loglik(engine, labels, params): + engine.set_params(params) + z = params["alpha"] + engine.scores() + return float(np.sum(np.asarray(labels, dtype=float) * z - np.logaddexp(0.0, z))) + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("mode", MODES) +def test_nwgrad_subgradient_matches_finite_differences(mode, gap_mode, substitution_mode, seed): + """nwgrad's gradient against central differences of nwgrad's own scores, no Biopython.""" + from src.nwgrad_backend import NwgradEngine + rng, seqsA, seqsB, labels = small_fixture(seed) + params = random_params(rng, gap_mode, substitution_mode) + engine = NwgradEngine(seqsA, seqsB, mode, gap_mode, substitution_mode, DNA, 1) + engine.set_params(params) + scores = engine.scores() + logits = 1 / (1 + np.exp(-(params["alpha"] + scores))) + grad = model_gradient(engine.raw_subgradient(logits, labels, params["alpha"]), + gap_mode, substitution_mode) + h = 1e-6 + for key, index, symmetric in _directions(params, substitution_mode): + up = _nwgrad_loglik(engine, labels, perturbed(params, key, h, index, symmetric)) + down = _nwgrad_loglik(engine, labels, perturbed(params, key, -h, index, symmetric)) + analytic = grad[key] if index is None else grad[key][index] + assert analytic == pytest.approx((up - down) / (2 * h), rel=1e-5, abs=1e-6), (key, index) + + +@pytest.mark.parametrize("mode", MODES) +def test_alpha_derivative_is_sum_of_residuals(mode): + rng, seqsA, seqsB, labels = small_fixture(11) + params = random_params(rng, "affine", "simple") + h = 1e-6 + up = loglik_at(seqsA, seqsB, labels, mode, dict(params, alpha=params["alpha"] + h)) + down = loglik_at(seqsA, seqsB, labels, mode, dict(params, alpha=params["alpha"] - h)) + aligner = make_aligner(mode, params) + scores = np.array([a.score for a in align_all(seqsA, seqsB, aligner)]) + residual = np.sum(labels - 1 / (1 + np.exp(-(params["alpha"] + scores)))) + assert residual == pytest.approx((up - down) / (2 * h), rel=1e-5, abs=1e-7) + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("mode", MODES) +def test_score_is_convex_in_parameters(mode, seed): + """Viterbi score is a max of linear functions, hence convex along any line.""" + rng, seqsA, seqsB, _ = small_fixture(seed) + p0 = random_params(rng, "affine", "general") + p1 = random_params(rng, "affine", "general") + + def mix(t): + M = (1 - t) * np.asarray(p0["substitution_matrix"]) + t * np.asarray(p1["substitution_matrix"]) + out = {k: (1 - t) * p0[k] + t * p1[k] for k in ("open_gap_score", "extend_gap_score")} + out["substitution_matrix"] = substitution_matrices.Array(alphabet=DNA, data=M) + return out + + def scores(t): + return np.array([a.score for a in align_all(seqsA, seqsB, make_aligner(mode, mix(t)))]) + + s0, s_half, s1 = scores(0.0), scores(0.5), scores(1.0) + assert np.all(s_half <= (s0 + s1) / 2 + 1e-9) + + +def test_symmetric_directional_derivative_counts_both_orientations(): + """For a symmetric matrix, d/dM[a,b] (a != b) sees A->B and B->A substitutions.""" + aligner = PairwiseAligner() + aligner.mode = "global" + aligner.open_gap_score = -10 + aligner.extend_gap_score = -10 + aligner.match_score = 1 + aligner.mismatch_score = -1 + aln = align_all(["AC"], ["CA"], aligner)[0] + sg = logit_subgradient([aln], [0.0], [1], 0.0, DNA) + grad = model_gradient(sg, "affine", "symmetric") + M = grad["substitution_matrix"] + assert M[0, 1] == 2 and M[1, 0] == 2 + assert np.trace(M) == 0 diff --git a/tests/test_inference.py b/tests/test_inference.py new file mode 100644 index 0000000..f66da20 --- /dev/null +++ b/tests/test_inference.py @@ -0,0 +1,68 @@ +import csv +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from src.discrimalign import discrimalign +from src.inference import load_model, predict_csv, predict_pairs, save_model + + +def _fit_model(): + return discrimalign( + seqlistA=["AUGCUA", "CUGA"], + seqlistB=["AUGGUA", "CUGU"], + labels=[1, 0], + max_iter=1, + num_threads=1, + ) + + +def test_predict_pairs_returns_serializable_alignment_rows(): + rows = predict_pairs(["AUGCUA"], ["AUGGUA"], _fit_model()) + + assert rows[0]["index"] == 0 + assert rows[0]["sequence_a"] == "AUGCUA" + assert rows[0]["sequence_b"] == "AUGGUA" + assert 0.0 <= rows[0]["probability"] <= 1.0 + assert rows[0]["aligned_sequence_a"] + assert rows[0]["alignment_marks"] + assert rows[0]["aligned_sequence_b"] + assert len(rows[0]["operations"]) == len(rows[0]["alignment_marks"]) + + +def test_predict_pairs_validates_pair_counts(): + try: + predict_pairs(["A"], ["A", "C"], _fit_model()) + except ValueError as exc: + assert "same length" in str(exc) + else: + raise AssertionError("Expected pair-count validation error") + + +def test_save_load_model_and_predict_csv(tmp_path): + model_path = tmp_path / "model.pkl" + input_path = tmp_path / "pairs.csv" + output_path = tmp_path / "predictions.csv" + + save_model(_fit_model(), model_path) + model = load_model(model_path) + + input_path.write_text("id,sequence_a,sequence_b\nexample,AUGCUA,AUGGUA\n") + rows = predict_csv(input_path, output_path, model) + + assert len(rows) == 1 + with output_path.open(newline="") as output_file: + output_rows = list(csv.DictReader(output_file)) + assert output_rows[0]["id"] == "example" + assert output_rows[0]["probability"] + assert output_rows[0]["alignment_marks"] + + +def test_load_model_supports_case_study_schema(): + model = load_model("case_study_for_mirna/trained_models/manakov_best_model.pkl") + rows = predict_pairs(["ATGCTA"], ["ATGGTA"], model) + + assert len(rows) == 1 + assert 0.0 <= rows[0]["probability"] <= 1.0 + assert rows[0]["alignment_marks"] diff --git a/tests/test_learning.py b/tests/test_learning.py new file mode 100644 index 0000000..f08d4ef --- /dev/null +++ b/tests/test_learning.py @@ -0,0 +1,95 @@ +""" +End-to-end learning on simulated data. These are the only tests that need +more than a handful of pairs, and they are marked slow. +""" +import warnings + +import numpy as np +import pytest +from sklearn.metrics import roc_auc_score + +from src.discrimalign import discrimalign +from src.optimization import create_powerstep +from tests.helpers import (GAP_MODES, MODES, SUBSTITUTION_MODES, align_all, make_pairs, + mutate, random_seq) + +pytestmark = pytest.mark.slow + +ALL_MODES = [(m, g, s) for m in MODES for g in GAP_MODES for s in SUBSTITUTION_MODES] +BACKENDS = ["biopython", "nwgrad"] + + +def _fit(A, B, y, mode, gap_mode, substitution_mode, backend, max_iter=30, scale=1e-3): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return discrimalign(A, B, y, aligner_mode=mode, gap_mode=gap_mode, + substitution_mode=substitution_mode, backend=backend, + stepfunction=create_powerstep(scale), max_iter=max_iter) + + +def _auc(aligner, A, B, y): + return roc_auc_score(y, [a.score for a in align_all(A, B, aligner)]) + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_learning_improves_fit_and_generalizes(mode, gap_mode, substitution_mode, backend): + rng = np.random.default_rng(0) + A, B, y = make_pairs(rng, 40, 40, 40, sub_rate=0.3, indel_rate=0.1) + res = _fit(A, B, y, mode, gap_mode, substitution_mode, backend) + + traj = res["loglik_trajectory"] + assert res["final_loglik"] > traj[0] + assert np.all(np.isfinite(traj)) + assert _auc(res["aligner"], A, B, y) >= 0.95 + + held_out = make_pairs(np.random.default_rng(1), 20, 20, 40, sub_rate=0.3, indel_rate=0.1) + assert _auc(res["aligner"], *held_out) >= 0.9 + + if substitution_mode == "simple": + assert res["match_score"] > res["mismatch_score"] + else: + M = np.asarray(res["substitution_matrix"]) + assert np.min(np.diag(M)) > np.mean(M[~np.eye(4, dtype=bool)]) + # In global mode, adding c to every substitution score and c/2 to every + # gap column shifts each score by c * (len A + len B) / 2. With equal + # lengths alpha absorbs that, so absolute signs are not identified. + # Local alignment breaks the symmetry. + if mode == "local": + if gap_mode == "affine": + assert res["open_gap_score"] < 0 + assert res["extend_gap_score"] < 0 + else: + assert res["gap_score"] < 0 + if substitution_mode == "simple": + assert res["match_score"] > 0 > res["mismatch_score"] + + +_TRANSITION = {"A": "G", "G": "A", "C": "C", "T": "T"} + + +def _transitions_only(rng, seq, rate): + return "".join(_TRANSITION[c] if c in "AG" and rng.random() < rate else c for c in seq) + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("substitution_mode", ["symmetric", "general"]) +def test_planted_substitution_is_recovered(substitution_mode, backend): + """Homologs differ only by A<->G transitions, so A-G must outscore every other mismatch.""" + rng = np.random.default_rng(0) + A, B, y = [], [], [] + for _ in range(40): + seq = random_seq(rng, 40) + A.append(seq) + B.append(mutate(rng, _transitions_only(rng, seq, 0.5), sub_rate=0.0, indel_rate=0.05)) + y.append(1) + A.append(random_seq(rng, 40)) + B.append(random_seq(rng, 40)) + y.append(0) + res = _fit(A, B, np.array(y), "local", "affine", substitution_mode, backend) + + M = res["substitution_matrix"] + others = [M[a, b] for a in "ACGT" for b in "ACGT" + if a != b and {a, b} != {"A", "G"}] + assert min(M["A", "G"], M["G", "A"]) > max(others) + 0.3 + assert min(M["A", "G"], M["G", "A"]) > 0 diff --git a/tests/test_logit_link.py b/tests/test_logit_link.py new file mode 100644 index 0000000..3c27d75 --- /dev/null +++ b/tests/test_logit_link.py @@ -0,0 +1,361 @@ +"""Unit tests for src.logit_link.""" +import numpy as np +import pytest +from Bio.Align import PairwiseAligner +from scipy.special import expit + +from src.logit_link import (d2lda2, dlda, fit_alpha, logit_logL, logit_partial_scores, + logit_subgradient) +from src.optimization import EmptyLocalAlignment +from tests.helpers import optimal_alpha + + +# --- logit_partial_scores --------------------------------------------------- + +def test_partial_scores_is_sigmoid_of_shifted_scores(): + scores = [-3.0, -0.5, 0.0, 1.25, 4.0] + alpha = 0.7 + expected = [1.0 / (1.0 + np.exp(-(alpha + s))) for s in scores] + np.testing.assert_allclose(logit_partial_scores(scores, alpha), expected, rtol=1e-14) + + +def test_partial_scores_zero_gives_half(): + np.testing.assert_allclose(logit_partial_scores([0.0, -2.0], [0.0, 2.0]), [0.5, 0.5]) + + +def test_partial_scores_accepts_scalar_and_returns_array(): + out = logit_partial_scores(1.0, 0.0) + assert isinstance(out, (np.ndarray, np.floating)) + assert out == pytest.approx(1.0 / (1.0 + np.exp(-1.0))) + + +def test_partial_scores_accepts_integer_scores(): + out = logit_partial_scores([0, 1, 2], 0) + assert out.dtype == float + + +def test_partial_scores_extreme_values_saturate_without_nan(): + out = logit_partial_scores([-1e4, 1e4], 0.0) + assert np.all(np.isfinite(out)) + assert out[0] == 0.0 + assert out[1] == 1.0 + + +def test_partial_scores_monotone_in_score(): + scores = np.linspace(-10, 10, 41) + out = logit_partial_scores(scores, -1.3) + assert np.all(np.diff(out) > 0) + + +def test_partial_scores_empty_input(): + assert logit_partial_scores([], 0.0).shape == (0,) + + +# --- logit_logL ------------------------------------------------------------- + +def test_logL_matches_bernoulli_formula(): + scores = np.array([-2.2, -0.4, 1.4, 2.9]) + alpha = 0.1 + y = np.array([0, 1, 1, 0]) + p = expit(alpha + scores) + expected = np.sum(y * np.log(p) + (1 - y) * np.log(1 - p)) + assert logit_logL(scores, alpha, y) == pytest.approx(expected, rel=1e-14) + + +def test_logL_returns_python_float(): + assert type(logit_logL([0.3], 0.0, [1])) is float + + +def test_logL_is_nonpositive(): + rng = np.random.default_rng(0) + scores = rng.normal(scale=4, size=50) + y = rng.integers(0, 2, size=50) + assert logit_logL(scores, 0.5, y) <= 0.0 + + +def test_logL_perfect_prediction_is_near_zero(): + assert logit_logL([50.0, -50.0], 0.0, [1, 0]) == pytest.approx(0.0, abs=1e-12) + + +def test_logL_is_exact_on_certain_wrong_predictions(): + # Probabilities would round to 0 and 1 here; the logits keep the exact value. + assert logit_logL([-800.0, 800.0], 0.0, [1, 0]) == -1600.0 + + +def test_logL_keeps_precision_on_confident_right_predictions(): + # log(1 - p) with p = expit(-40) would round to 0; the exact value is about -4e-18. + assert logit_logL([40.0], 0.0, [1]) == pytest.approx(-np.log1p(np.exp(-40.0)), rel=1e-14) + + +@pytest.mark.parametrize("scores,alpha", [([0.0, np.nan], 0.0), ([0.0, np.inf], 0.0), + ([0.0, 1.0], -np.inf), ([0.0, 1.0], np.nan)]) +def test_logL_raises_on_non_finite_input(scores, alpha): + with pytest.raises(FloatingPointError): + logit_logL(scores, alpha, [0, 1]) + + +def test_logL_accepts_lists(): + assert logit_logL([-1.0, 1.0], 0.0, [0, 1]) == pytest.approx(2 * np.log(expit(1.0))) + + +def test_logL_accepts_boolean_labels(): + assert logit_logL([-1.0, 1.0], 0.0, np.array([False, True])) == pytest.approx( + 2 * np.log(expit(1.0))) + + +def test_logL_adds_alpha_to_every_score(): + scores = np.array([-1.5, 0.2, 3.0]) + y = [0, 1, 1] + assert logit_logL(scores, 0.7, y) == logit_logL(scores + 0.7, 0.0, y) + + +@pytest.mark.parametrize("labels", [[0, 2], [-1, 1], [0.5, 1]]) +def test_logL_rejects_non_binary_labels(labels): + with pytest.raises(ValueError): + logit_logL([0.3, 0.6], 0.0, labels) + + +def test_logL_empty_is_zero(): + assert logit_logL([], 0.0, []) == 0.0 + + +def test_logL_unchecked_is_bit_identical_to_logL(): + from src.logit_link import _logit_logL_unchecked + rng = np.random.default_rng(4) + scores = rng.normal(scale=20, size=1000) + labels = rng.integers(0, 2, 1000) + assert _logit_logL_unchecked(scores, -0.3, labels.astype(float)) == logit_logL(scores, -0.3, labels) + + +# --- dlda and d2lda2 -------------------------------------------------------- + +def _logL_of_alpha(alpha, scores, labels): + return logit_logL(scores, alpha, labels) + + +@pytest.mark.parametrize("seed", range(5)) +def test_dlda_matches_finite_difference(seed): + rng = np.random.default_rng(seed) + scores = rng.normal(0, 3, size=20) + labels = rng.integers(0, 2, size=20) + alpha, h = rng.normal(), 1e-6 + numeric = (_logL_of_alpha(alpha + h, scores, labels) + - _logL_of_alpha(alpha - h, scores, labels)) / (2 * h) + analytic = dlda(logit_partial_scores(scores, alpha), labels) + assert analytic == pytest.approx(numeric, rel=1e-6, abs=1e-8) + + +@pytest.mark.parametrize("seed", range(5)) +def test_d2lda2_matches_finite_difference(seed): + rng = np.random.default_rng(seed) + scores = rng.normal(0, 3, size=20) + labels = rng.integers(0, 2, size=20) + alpha, h = rng.normal(), 1e-5 + d = lambda a: dlda(logit_partial_scores(scores, a), labels) + numeric = (d(alpha + h) - d(alpha - h)) / (2 * h) + analytic = d2lda2(logit_partial_scores(scores, alpha), labels) + assert analytic == pytest.approx(numeric, rel=1e-6) + + +def test_d2lda2_is_negative(): + p = np.array([0.2, 0.5, 0.9]) + assert d2lda2(p, [0, 1, 1]) < 0 + + +def test_dlda_vanishes_when_mean_prediction_matches_mean_label(): + assert dlda(np.array([0.25, 0.75]), np.array([0, 1])) == pytest.approx(0.0) + + +# --- fit_alpha ------------------------------------------------------------- + +def _alpha_data(seed, n=500, offset=0.0): + rng = np.random.default_rng(seed) + scores = rng.normal(-3.0, 2.0, n) + offset + labels = (rng.random(n) < 1.0 / (1.0 + np.exp(-(scores - offset + 1.5)))).astype(int) + return scores, labels + + +@pytest.mark.parametrize("seed", range(4)) +@pytest.mark.parametrize("start", [0.0, 1e-3, 0.5, -5.0, 20.0, -500.0, 500.0]) +def test_fit_alpha_finds_the_root_from_any_start(seed, start): + scores, labels = _alpha_data(seed) + ref = optimal_alpha(scores, labels) + alpha = fit_alpha(scores, labels, ref + start) + assert alpha == pytest.approx(ref, abs=1e-10) + assert abs(dlda(logit_partial_scores(scores, alpha), labels)) < 1e-8 + + +@pytest.mark.parametrize("offset", [300.0, -300.0, 1600.0]) +def test_fit_alpha_follows_a_large_jump_of_the_optimum(offset): + """Every score shifted far, as after an unscaled step on large data: alpha* moves by -offset.""" + scores, labels = _alpha_data(1, offset=offset) + ref = optimal_alpha(scores, labels, bracket=(-offset - 200.0, -offset + 200.0)) + assert fit_alpha(scores, labels, 0.0) == pytest.approx(ref, abs=1e-9) + + +def test_fit_alpha_does_not_depend_on_the_start(): + scores, labels = _alpha_data(2) + results = [fit_alpha(scores, labels, start) for start in (-50.0, -1.0, 0.0, 3.0, 50.0)] + assert max(results) - min(results) < 1e-10 + + +def test_fit_alpha_matches_mean_label_when_scores_are_equal(): + labels = np.array([1, 0, 0, 1, 1, 0, 1, 1]) + alpha = fit_alpha(np.zeros(len(labels)), labels, 0.0) + assert 1.0 / (1.0 + np.exp(-alpha)) == pytest.approx(labels.mean(), abs=1e-12) + + +def test_fit_alpha_returns_python_float_and_accepts_lists(): + alpha = fit_alpha([0.0, 1.0, -1.0, 2.0], [1, 0, 0, 1], 0) + assert isinstance(alpha, float) + + +@pytest.mark.parametrize("start", [0.0, 0.3, -40.0]) +def test_fit_alpha_given_starting_probabilities_is_bit_identical(start): + scores, labels = _alpha_data(3) + expected = fit_alpha(scores, labels, start) + given = fit_alpha(scores, labels, start, logit_scores0=logit_partial_scores(scores, start)) + assert given == expected + + +@pytest.mark.parametrize("labels", [[1, 1, 1], [0, 0, 0]]) +def test_fit_alpha_needs_both_classes(labels): + with pytest.raises(ValueError, match="both classes"): + fit_alpha([0.0, 1.0, 2.0], labels, 0.0) + + +# --- logit_subgradient on hand-built alignments ----------------------------- +# +# logit_subgradient only indexes aln[0] and aln[1], so a pair of gapped +# strings stands in for an Alignment object. + +def _single(aln, alphabet="ACGT", weight=1.0): + """Subgradient of one alignment with label - logit_score == weight.""" + return logit_subgradient([aln], [0.0], [weight], 0.0, alphabet) + + +def test_subgradient_counts_matches_and_mismatches(): + sg = _single(("ACGT", "ACCT")) + G = np.asarray(sg["Substitutions"]) + assert G.sum() == 4 + assert sg["Substitutions"]["A", "A"] == 1 + assert sg["Substitutions"]["C", "C"] == 1 + assert sg["Substitutions"]["G", "C"] == 1 + assert sg["Substitutions"]["C", "G"] == 0 # direction matters + assert sg["Substitutions"]["T", "T"] == 1 + assert sg["Gap opens"] == 0 + assert sg["Gap extends"] == 0 + + +def test_subgradient_matrix_carries_alphabet_order(): + sg = _single(("AC", "AC"), alphabet="TGCA") + assert "".join(sg["Substitutions"].alphabet) == "TGCA" + assert np.asarray(sg["Substitutions"])[3, 3] == 1 # A is last + + +@pytest.mark.parametrize("aln, opens, extends", [ + (("A-C", "AGC"), 1, 0), + (("A--C", "AGGC"), 1, 1), + (("A---C", "AGGGC"), 1, 2), + (("AGC", "A-C"), 1, 0), + (("AGGGC", "A---C"), 1, 2), + (("-AC", "GAC"), 1, 0), # leading gap + (("AC--", "ACGT"), 1, 1), # trailing gap + (("A-C-G", "ATCTG"), 2, 0), # two gaps separated by a match + (("A-CG", "AT-G"), 2, 0), # gap in A, then gap in B + (("A--C--", "AGTCGT"), 2, 2), + (("--", "AC"), 1, 1), # all gaps +]) +def test_subgradient_gap_counts(aln, opens, extends): + sg = _single(aln) + assert sg["Gap opens"] == opens + assert sg["Gap extends"] == extends + + +def test_subgradient_alternating_gaps_are_all_opens(): + """Switching between gap-in-A and gap-in-B opens a new gap each time.""" + sg = _single(("-A-", "C-G")) + assert sg["Gap opens"] == 3 + assert sg["Gap extends"] == 0 + + +def test_subgradient_gap_after_match_reopens(): + sg = _single(("A-CC-A", "AGCCTA")) + assert sg["Gap opens"] == 2 + assert sg["Gap extends"] == 0 + + +def test_subgradient_weight_is_label_minus_logit(): + sg = logit_subgradient([("AC", "AC")], [0.3], [1], 0.0, "ACGT") + assert sg["Substitutions"]["A", "A"] == pytest.approx(0.7) + sg = logit_subgradient([("AC", "AC")], [0.3], [0], 0.0, "ACGT") + assert sg["Substitutions"]["A", "A"] == pytest.approx(-0.3) + + +def test_subgradient_ignores_alpha_argument(): + a = logit_subgradient([("A-C", "AGC")], [0.4], [1], -5.0, "ACGT") + b = logit_subgradient([("A-C", "AGC")], [0.4], [1], 7.0, "ACGT") + np.testing.assert_array_equal(np.asarray(a["Substitutions"]), np.asarray(b["Substitutions"])) + assert a["Gap opens"] == b["Gap opens"] + + +def test_subgradient_sums_over_alignments_linearly(): + alns = [("A-C", "AGC"), ("GGT", "GAT"), ("AC--", "ACGT")] + logits = [0.2, 0.9, 0.5] + labels = [1, 0, 1] + total = logit_subgradient(alns, logits, labels, 0.0, "ACGT") + parts = [logit_subgradient([a], [p], [y], 0.0, "ACGT") + for a, p, y in zip(alns, logits, labels)] + np.testing.assert_allclose(np.asarray(total["Substitutions"]), + sum(np.asarray(p["Substitutions"]) for p in parts)) + assert total["Gap opens"] == pytest.approx(sum(p["Gap opens"] for p in parts)) + assert total["Gap extends"] == pytest.approx(sum(p["Gap extends"] for p in parts)) + + +def test_subgradient_zero_when_predictions_are_exact(): + sg = logit_subgradient([("ACG", "A-G"), ("TT", "TA")], [1.0, 0.0], [1, 0], 0.0, "ACGT") + assert np.all(np.asarray(sg["Substitutions"]) == 0) + assert sg["Gap opens"] == 0 + assert sg["Gap extends"] == 0 + + +def test_subgradient_of_empty_local_alignment_is_zero(): + sg = logit_subgradient([EmptyLocalAlignment()], [0.2], [1], 0.0, "ACGT") + assert np.all(np.asarray(sg["Substitutions"]) == 0) + assert sg["Gap opens"] == 0 + assert sg["Gap extends"] == 0 + + +def test_subgradient_empty_list(): + sg = logit_subgradient([], [], [], 0.0, "AC") + assert np.asarray(sg["Substitutions"]).shape == (2, 2) + assert sg["Gap opens"] == 0 + + +def test_subgradient_rejects_length_mismatch(): + with pytest.raises(AssertionError): + logit_subgradient([("A", "A")], [0.1, 0.2], [1, 0], 0.0, "ACGT") + with pytest.raises(AssertionError): + logit_subgradient([("A", "A")], [0.1], [1, 0], 0.0, "ACGT") + + +def test_subgradient_rejects_character_outside_alphabet(): + with pytest.raises((KeyError, IndexError, ValueError)): + _single(("AX", "AA"), alphabet="ACGT") + + +def test_subgradient_on_biopython_alignment(): + aligner = PairwiseAligner() + aligner.mode = "global" + aligner.match_score = 2 + aligner.mismatch_score = -1 + aligner.open_gap_score = -3 + aligner.extend_gap_score = -1 + aln = next(aligner.align("ACGTTGCA", "ACGGCA")) + sg = _single(aln) + G = np.asarray(sg["Substitutions"]) + counts = aln.counts() + assert np.trace(G) == counts.identities + assert G.sum() - np.trace(G) == counts.mismatches + assert sg["Gap opens"] == counts.open_gaps + assert sg["Gap extends"] == counts.extend_gaps diff --git a/tests/test_mirbench.py b/tests/test_mirbench.py new file mode 100644 index 0000000..55078e8 --- /dev/null +++ b/tests/test_mirbench.py @@ -0,0 +1,83 @@ +""" +Short run on the miRBench datasets. Similar to a standard analysis. + +Slow and optional: it downloads miRBench datasets and fits every mode on the full +training sets (millions of pairs), so it runs only with DISCRIMALIGN_RUN_MIRBENCH=1. +""" +import os + +import numpy as np +import pytest +from Bio.Seq import Seq +from Bio.SeqRecord import SeqRecord + +from miRBench.dataset import list_datasets, get_dataset_df +from src.discrimalign import discrimalign +from src.optimization import create_powerstep, create_constant_step +from tests.helpers import (GAP_MODES, MODES, SUBSTITUTION_MODES, align_all, make_pairs, + mutate, random_seq) + +ALL_MODES = [(m, g, s) for m in MODES for g in GAP_MODES for s in SUBSTITUTION_MODES] +BACKENDS = ["biopython", "nwgrad"] +DATASET_IDs = [0, 2] + +def _get_data_and_params(dset_id): + assert dset_id in {0, 2} + train = get_dataset_df(list_datasets()[dset_id], split="train") + mirlist = train['noncodingRNA'] + mirlist = [str(Seq(seq)) for seq in mirlist] + genelist = train['gene'] + genelist = [str(Seq(seq).reverse_complement()) for seq in genelist] + label_list = list(train['label']) + if dset_id == 0: + stepfunction = create_constant_step(0.00005) + else: + stepfunction = create_constant_step(0.0000005) + return (mirlist, genelist, label_list, stepfunction) + +NITER = 20 + +pytestmark = [ + pytest.mark.slow, + pytest.mark.skipif(os.environ.get("DISCRIMALIGN_RUN_MIRBENCH") != "1", + reason="miRBench end-to-end runs: set DISCRIMALIGN_RUN_MIRBENCH=1"), +] + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("dset_id", DATASET_IDs) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_mirbench_run(dset_id, mode, gap_mode, substitution_mode, backend): + """ + Check if discrimalign runs properly using data preprocessed with BioPython. + """ + mirlist, genelist, labels, stepfunction = _get_data_and_params(dset_id) + res = discrimalign(mirlist, genelist, labels, + stepfunction=stepfunction, + aligner_mode=mode, + substitution_mode=substitution_mode, + gap_mode=gap_mode, + stochastic_factor=0.01, + verbose=True, max_iter=NITER, + backend=backend) + traj = res["loglik_trajectory"] + assert res["final_loglik"] > traj[0] + assert np.all(np.isfinite(traj)) + if substitution_mode == "simple": + assert res["match_score"] > res["mismatch_score"] + else: + M = np.asarray(res["substitution_matrix"]) + assert np.min(np.diag(M)) > np.mean(M[~np.eye(4, dtype=bool)]) + # In global mode, adding c to every substitution score and c/2 to every + # gap column shifts each score by c * (len A + len B) / 2. With equal + # lengths alpha absorbs that, so absolute signs are not identified. + # Local alignment breaks the symmetry. + if mode == "local": + if gap_mode == "affine": + assert res["open_gap_score"] < 0 + assert res["extend_gap_score"] < 0 + else: + assert res["gap_score"] < 0 + if substitution_mode == "simple": + assert res["match_score"] > 0 > res["mismatch_score"] + + diff --git a/tests/test_nwgrad_backend.py b/tests/test_nwgrad_backend.py new file mode 100644 index 0000000..6bf7b12 --- /dev/null +++ b/tests/test_nwgrad_backend.py @@ -0,0 +1,455 @@ +""" +The nwgrad backend must follow the Biopython backend's parameter trajectory. + +Both backends find optimal paths at the same parameters. Where raw counts can +differ (ties in simple or linear mode), the gradient in the model's own +parameters does not, so trajectories agree up to rounding. +""" +import sys +import warnings + +import numpy as np +import nwgrad +import pytest +from Bio.Align import PairwiseAligner, substitution_matrices + +from src.discrimalign import _MAX_GAP_SCORE, discrimalign +from src.nwgrad_backend import baseline_parameters +from src.optimization import (EmptyLocalAlignment, create_constant_step, create_powerstep, + get_initial_estimate_from_counts) +from tests.helpers import (GAP_MODES, MODES, SUBSTITUTION_MODES, default_baseline, + make_aligner, make_pairs, random_params) + +ALL_MODES = [(m, g, s) for m in MODES for g in GAP_MODES for s in SUBSTITUTION_MODES] + +TRAJECTORY_KEYS = ("loglik_trajectory", "subgradient_l2_trajectory", + "alignment_logit_scores") + + +def _data(seed, n=12, length=12): + rng = np.random.default_rng(seed) + A, B, y = make_pairs(rng, n // 2, n - n // 2, length, sub_rate=0.25) + y[np.flatnonzero(y == 1)[0]] = 0 + y[np.flatnonzero(y == 0)[0]] = 1 + return rng, A, B, y + + +def _param_keys(gap_mode, substitution_mode): + keys = {"open_gap_score", "extend_gap_score"} if gap_mode == "affine" else {"gap_score"} + keys |= {"match_score", "mismatch_score"} if substitution_mode == "simple" else {"substitution_matrix"} + return keys | {"alpha", "final_loglik"} + + +def _both(A, B, y, mode, gap_mode, substitution_mode, **kwargs): + kwargs.setdefault("stepfunction", create_constant_step(0.01)) + kwargs.setdefault("max_iter", 3) + runs = {} + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + for backend in ("biopython", "nwgrad"): + runs[backend] = discrimalign(A, B, y, aligner_mode=mode, gap_mode=gap_mode, + substitution_mode=substitution_mode, + backend=backend, **kwargs) + return runs["biopython"], runs["nwgrad"] + + +def _assert_same_fit(bio, nw, gap_mode, substitution_mode, rtol=1e-9, atol=1e-9): + for key in _param_keys(gap_mode, substitution_mode) | set(TRAJECTORY_KEYS): + np.testing.assert_allclose(np.asarray(nw[key], dtype=float), np.asarray(bio[key], dtype=float), + rtol=rtol, atol=atol, err_msg=key) + + +@pytest.mark.parametrize("seed", [0, 1]) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_trajectory_matches_biopython_from_given_parameters(mode, gap_mode, substitution_mode, seed): + rng, A, B, y = _data(seed) + p0 = random_params(rng, gap_mode, substitution_mode) + bio, nw = _both(A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0) + _assert_same_fit(bio, nw, gap_mode, substitution_mode) + + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_trajectory_matches_biopython_from_initial_estimate(mode, gap_mode, substitution_mode): + """From a tie-free baseline, both backends fit the same initial estimate.""" + rng, A, B, y = _data(2, n=20) + baseline = default_baseline(mode, gap_mode, substitution_mode, perturb=rng) + bio, nw = _both(A, B, y, mode, gap_mode, substitution_mode, max_iter=3, + stepfunction=create_powerstep(1e-3), baseline_aligner=baseline) + _assert_same_fit(bio, nw, gap_mode, substitution_mode) + + +@pytest.mark.parametrize("seed", [0, 1]) +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_initial_estimate_matches_biopython_from_tie_free_baseline(mode, gap_mode, + substitution_mode, seed): + rng, A, B, y = _data(20 + seed, n=20) + baseline = default_baseline(mode, gap_mode, substitution_mode, perturb=rng) + bio, nw = _both(A, B, y, mode, gap_mode, substitution_mode, max_iter=0, + stepfunction=None, baseline_aligner=baseline) + _assert_same_fit(bio, nw, gap_mode, substitution_mode, rtol=1e-12, atol=1e-12) + + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_nwgrad_initial_estimate_is_fitted_on_nwgrad_counts(mode, gap_mode, substitution_mode): + """With the default (tie-prone) baseline: self-consistent, whatever ties nwgrad broke.""" + from src.nwgrad_backend import NwgradEngine, baseline_parameters + _, A, B, y = _data(3, n=20) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + res = discrimalign(A, B, y, aligner_mode=mode, gap_mode=gap_mode, + substitution_mode=substitution_mode, max_iter=0, backend="nwgrad") + engine = NwgradEngine(A, B, mode, gap_mode, substitution_mode, "ACGT", 1) + engine.set_params(baseline_parameters(default_baseline(mode, gap_mode, substitution_mode), + gap_mode, "ACGT"), substitution_mode="general") + engine.scores() + expected = get_initial_estimate_from_counts(engine.raw_counts(), y, substitution_mode, + gap_mode, "ACGT") + for key in _param_keys(gap_mode, substitution_mode) - {"alpha", "final_loglik"}: + value = np.asarray(expected[key], dtype=float) + if key in ("open_gap_score", "extend_gap_score", "gap_score"): + value = np.minimum(value, _MAX_GAP_SCORE) + np.testing.assert_allclose(np.asarray(res[key], dtype=float), value, rtol=1e-12, err_msg=key) + assert res["alpha"] == pytest.approx(expected["alpha"], rel=1e-12) + + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", ALL_MODES) +def test_count_arrays_equal_raw_counts(mode, gap_mode, substitution_mode): + """count_arrays() gives exactly the per-pair counts of raw_counts().""" + from src.nwgrad_backend import NwgradEngine + from src.optimization import _count_arrays_from_raw + rng, A, B, _ = _data(5, n=20) + engine = NwgradEngine(A, B, mode, gap_mode, substitution_mode, "ACGT", 2) + baseline = baseline_parameters(default_baseline(mode, gap_mode, substitution_mode), + gap_mode, "ACGT") + for params, params_mode in ((baseline, "general"), + (random_params(rng, gap_mode, substitution_mode), None)): + engine.set_params(params, substitution_mode=params_mode) + engine.scores() + counts = engine.count_arrays() + expected = _count_arrays_from_raw(engine.raw_counts()) + assert counts.alphabet == expected.alphabet + for field in ("substitutions", "gap_opens", "gap_extends"): + np.testing.assert_array_equal(getattr(counts, field), getattr(expected, field), + err_msg=field) + + +@pytest.mark.parametrize("initial", [True, False]) +def test_nwgrad_backend_needs_no_biopython_alignment(initial, monkeypatch): + """Without returned alignments, the nwgrad path never aligns with Biopython.""" + module = sys.modules["src.discrimalign"] + + def fail(*args, **kwargs): + raise AssertionError("Biopython alignment on the nwgrad path") + + monkeypatch.setattr(module, "_align_pairs", fail) + rng, A, B, y = _data(4, n=20) + kwargs = {"initial_parameters": random_params(rng, "affine", "general")} if initial else {} + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + res = discrimalign(A, B, y, aligner_mode="local", gap_mode="affine", + substitution_mode="general", max_iter=2, return_alignments=False, + stepfunction=create_constant_step(0.01), backend="nwgrad", **kwargs) + assert "alignments" not in res + + +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +def test_trajectory_matches_with_seeded_noise(substitution_mode): + rng, A, B, y = _data(3) + p0 = random_params(rng, "affine", substitution_mode) + runs = [] + for backend in ("biopython", "nwgrad"): + np.random.seed(11) + runs.append(discrimalign(A, B, y, aligner_mode="local", gap_mode="affine", + substitution_mode=substitution_mode, initial_parameters=p0, + stochastic_factor=0.3, max_iter=3, + stepfunction=create_constant_step(0.01), backend=backend)) + _assert_same_fit(runs[0], runs[1], "affine", substitution_mode) + + +def test_baseline_aligner_mode_takes_precedence_in_both_backends(): + rng, A, B, y = _data(4) + baseline = make_aligner("global", random_params(rng, "affine", "simple")) + bio, nw = _both(A, B, y, "local", "affine", "simple", baseline_aligner=baseline) + assert bio["aligner"].mode == nw["aligner"].mode == "global" + _assert_same_fit(bio, nw, "affine", "simple") + + +@pytest.mark.parametrize("num_threads", [2, 5]) +def test_nwgrad_thread_count_does_not_change_results(num_threads): + rng, A, B, y = _data(5, n=20) + p0 = random_params(rng, "affine", "general") + kwargs = dict(aligner_mode="local", gap_mode="affine", substitution_mode="general", + initial_parameters=p0, max_iter=3, stepfunction=create_constant_step(0.01), + backend="nwgrad") + one = discrimalign(A, B, y, num_threads=1, **kwargs) + many = discrimalign(A, B, y, num_threads=num_threads, **kwargs) + for key in _param_keys("affine", "general") | set(TRAJECTORY_KEYS): + np.testing.assert_array_equal(np.asarray(many[key]), np.asarray(one[key]), err_msg=key) + + +def test_nwgrad_local_pairs_without_alignment(): + A = ["AAAA", "ACGT", "CCCC", "ACGTAC", "GGGG", "ACGA"] + B = ["CCCC", "ACGT", "GGGG", "ACGTAC", "TTTT", "ACGT"] + y = np.array([0, 1, 0, 1, 0, 1]) + p0 = random_params(np.random.default_rng(6), "affine", "general") + bio, nw = _both(A, B, y, "local", "affine", "general", initial_parameters=p0) + _assert_same_fit(bio, nw, "affine", "general") + for i in (0, 2, 4): + assert isinstance(nw["alignments"][i], EmptyLocalAlignment) + + +def test_nwgrad_returns_biopython_aligner_and_alignments(): + rng, A, B, y = _data(7) + p0 = random_params(rng, "linear", "symmetric") + bio, nw = _both(A, B, y, "global", "linear", "symmetric", initial_parameters=p0) + assert type(nw["aligner"]) is type(bio["aligner"]) + assert [a.score for a in nw["alignments"]] == pytest.approx( + [a.score for a in bio["alignments"]], rel=1e-9) + + +def test_nwgrad_without_alignments(): + rng, A, B, y = _data(8) + _, nw = _both(A, B, y, "local", "affine", "simple", + initial_parameters=random_params(rng, "affine", "simple"), + return_alignments=False) + assert "alignments" not in nw + + +def test_default_backend_is_nwgrad(): + rng, A, B, y = _data(9) + p0 = random_params(rng, "affine", "symmetric") + kwargs = dict(aligner_mode="local", gap_mode="affine", substitution_mode="symmetric", + initial_parameters=p0, max_iter=2, stepfunction=create_constant_step(0.01)) + default = discrimalign(A, B, y, **kwargs) + explicit = discrimalign(A, B, y, backend="nwgrad", **kwargs) + for key in _param_keys("affine", "symmetric") | set(TRAJECTORY_KEYS): + np.testing.assert_array_equal(np.asarray(default[key]), np.asarray(explicit[key]), err_msg=key) + + +@pytest.mark.parametrize("gap_mode, params", [ + ("linear", {"gap_score": -1.0, "match_score": 2.0, "mismatch_score": -5.0}), + ("affine", {"open_gap_score": -1.0, "extend_gap_score": -0.5, + "match_score": 2.0, "mismatch_score": -5.0}), +]) +@pytest.mark.parametrize("a, b", [("AAA", "AAAT"), ("AAA", "TAAA"), ("AAAT", "AAA")]) +def test_local_terminal_gaps_agree_when_gaps_are_penalized(gap_mode, params, a, b): + from src.nwgrad_backend import NwgradEngine + engine = NwgradEngine([a], [b], "local", gap_mode, "simple", "ACGT", 1) + engine.set_params(params) + bio = make_aligner("local", params).score(a, b) + assert engine.scores()[0] == pytest.approx(bio) + + +@pytest.mark.parametrize("a, b", [ + pytest.param("AAA", "AAAT", marks=pytest.mark.xfail(strict=True, reason=( + "Positive gap scores are outside local alignment's domain: nwgrad lets a " + "local alignment end with a gap column, and Biopython's align() and " + "score() disagree with each other. discrimalign keeps gap scores negative."))), + ("AAA", "TAAA"), +]) +def test_local_terminal_gaps_agree_when_gaps_score_positive(a, b): + from src.nwgrad_backend import NwgradEngine + params = {"gap_score": 1.0, "match_score": 2.0, "mismatch_score": -5.0} + engine = NwgradEngine([a], [b], "local", "linear", "simple", "ACGT", 1) + engine.set_params(params) + assert engine.scores()[0] == pytest.approx(make_aligner("local", params).score(a, b)) + + +def test_unknown_backend_is_rejected(): + _, A, B, y = _data(10) + with pytest.raises(AssertionError): + discrimalign(A, B, y, max_iter=0, backend="parasail") + + +@pytest.mark.slow +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", + [("local", "affine", "symmetric"), ("global", "linear", "simple"), + ("local", "affine", "general")]) +def test_long_trajectory_matches_biopython(mode, gap_mode, substitution_mode): + """30 iterations on 80 pairs: rounding differences must not compound into divergence.""" + rng = np.random.default_rng(0) + A, B, y = make_pairs(rng, 40, 40, 40, sub_rate=0.3, indel_rate=0.1) + bio, nw = _both(A, B, y, mode, gap_mode, substitution_mode, max_iter=30, + stepfunction=create_powerstep(1e-3)) + _assert_same_fit(bio, nw, gap_mode, substitution_mode, rtol=1e-7, atol=1e-7) + + +# --- baseline_parameters: strict conversion of a Biopython aligner ---------- + +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +def test_baseline_parameters_of_default_aligner(gap_mode, substitution_mode): + p = baseline_parameters(default_baseline("local", gap_mode, substitution_mode), gap_mode, "ACGT") + if gap_mode == "affine": + assert (p["open_gap_score"], p["extend_gap_score"]) == (-8.0, -0.5) + else: + assert p["gap_score"] == -6.0 + assert "".join(p["substitution_matrix"].alphabet) == "ACGT" + np.testing.assert_array_equal(np.asarray(p["substitution_matrix"]), 9 * np.eye(4) - 4) + + +def test_baseline_parameters_reorders_matrix_to_alphabet(): + aligner = PairwiseAligner() + aligner.substitution_matrix = substitution_matrices.Array( + alphabet="TGCAN", data=np.arange(25.0).reshape(5, 5)) + M = baseline_parameters(aligner, "affine", "ACGT")["substitution_matrix"] + assert "".join(M.alphabet) == "ACGT" + src = aligner.substitution_matrix + for a in "ACGT": + for b in "ACGT": + assert M[a, b] == src[a, b] + + +def test_baseline_parameters_affine_aligner_in_affine_mode_of_linear_scores(): + aligner = PairwiseAligner() + aligner.gap_score = -3 + p = baseline_parameters(aligner, "affine", "ACGT") + assert (p["open_gap_score"], p["extend_gap_score"]) == (-3.0, -3.0) + + +def _aligner(**scores): + aligner = PairwiseAligner() + aligner.match_score, aligner.mismatch_score = 2, -1 + aligner.open_gap_score, aligner.extend_gap_score = -5, -1 + for name, value in scores.items(): + setattr(aligner, name, value) + return aligner + + +@pytest.mark.parametrize("gap_mode, scores, message", [ + ("affine", {"end_gap_score": 0}, "internal and end gaps"), + ("affine", {"open_insertion_score": -2}, "same for both sequences"), + ("linear", {}, "open must equal extend"), + ("affine", {"wildcard": "N"}, "wildcard"), + ("affine", {"mode": "fogsaa"}, "not supported"), +]) +def test_baseline_parameters_rejects_what_the_model_cannot_express(gap_mode, scores, message): + with pytest.raises(ValueError, match=message): + baseline_parameters(_aligner(**scores), gap_mode, "ACGT") + + +def test_baseline_parameters_rejects_matrix_missing_letters(): + aligner = PairwiseAligner() + aligner.substitution_matrix = substitution_matrices.Array(alphabet="ACG", data=np.eye(3)) + with pytest.raises(ValueError, match="lacks letters 'T'"): + baseline_parameters(aligner, "affine", "ACGT") + + +def test_nwgrad_backend_rejects_nonuniform_baseline_aligner(): + _, A, B, y = _data(5) + with pytest.raises(ValueError, match="internal and end gaps"): + discrimalign(A, B, y, aligner_mode="global", gap_mode="affine", substitution_mode="simple", + baseline_aligner=_aligner(end_gap_score=0), max_iter=0, backend="nwgrad") + + +# --- inputs beyond short DNA ------------------------------------------------ +# +# Continuous random starting parameters, so neither the start nor the fitted +# point has exactly equal scores that the backends could break differently. + +PROTEIN = "ACDEFGHIKLMNPQRSTVWY" + + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", + [("local", "affine", "symmetric"), ("global", "linear", "general")]) +def test_protein_trajectory_matches_biopython(mode, gap_mode, substitution_mode): + rng = np.random.default_rng(50) + A, B, y = make_pairs(rng, 5, 5, 60, alphabet=PROTEIN, sub_rate=0.3) + p0 = random_params(rng, gap_mode, substitution_mode, alphabet=PROTEIN) + bio, nw = _both(A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0, + alphabet=PROTEIN) + _assert_same_fit(bio, nw, gap_mode, substitution_mode) + + +@pytest.mark.parametrize("mode, gap_mode, substitution_mode", + [("local", "affine", "general"), ("global", "linear", "simple"), + ("global", "affine", "symmetric")]) +def test_long_sequence_trajectory_matches_biopython(mode, gap_mode, substitution_mode): + rng = np.random.default_rng(51) + A, B, y = make_pairs(rng, 3, 3, 400, sub_rate=0.2, indel_rate=0.05) + p0 = random_params(rng, gap_mode, substitution_mode) + bio, nw = _both(A, B, y, mode, gap_mode, substitution_mode, initial_parameters=p0) + _assert_same_fit(bio, nw, gap_mode, substitution_mode) + + +def test_lowercase_sequences_match_biopython(): + rng, A, B, y = _data(52) + A, B = [a.lower() for a in A], [b.lower() for b in B] + p0 = random_params(rng, "affine", "general", alphabet="acgt") + bio, nw = _both(A, B, y, "local", "affine", "general", initial_parameters=p0) + _assert_same_fit(bio, nw, "affine", "general") + + +@pytest.mark.parametrize("backend", ["biopython", "nwgrad"]) +def test_out_of_alphabet_letter_raises(backend): + _, A, B, y = _data(53) + A[2] = A[2][:3] + "X" + A[2][4:] + with pytest.raises(ValueError): + discrimalign(A, B, y, aligner_mode="local", gap_mode="affine", substitution_mode="general", + alphabet="ACGT", max_iter=1, stepfunction=create_constant_step(0.01), + backend=backend) + + +@pytest.mark.parametrize("explicit", [True, False]) +@pytest.mark.parametrize("backend", ["biopython", "nwgrad"]) +def test_automatic_thread_count(backend, explicit, monkeypatch): + """num_threads=0, the default: all logical cores with nwgrad, 1 with Biopython.""" + import src.nwgrad_backend as nwgrad_backend + module = sys.modules["src.discrimalign"] + seen = [] + engine_init = nwgrad_backend.NwgradEngine.__init__ + align_pairs = module._align_pairs + + def spy_engine(self, *args, **kwargs): + seen.append(args[-1]) + engine_init(self, *args, **kwargs) + + def spy_align(seqsA, seqsB, aligner, num_threads): + seen.append(num_threads) + return align_pairs(seqsA, seqsB, aligner, num_threads) + + monkeypatch.setattr(nwgrad_backend.NwgradEngine, "__init__", spy_engine) + monkeypatch.setattr(module, "_align_pairs", spy_align) + monkeypatch.setattr(module.os, "cpu_count", lambda: 3) + rng, A, B, y = _data(54) + p0 = random_params(rng, "affine", "general") + kwargs = dict(aligner_mode="local", gap_mode="affine", substitution_mode="general", + initial_parameters=p0, max_iter=2, stepfunction=create_constant_step(0.01), + backend=backend) + auto = discrimalign(A, B, y, **({"num_threads": 0} if explicit else {}), **kwargs) + expected = 3 if backend == "nwgrad" else 1 + assert seen and set(seen) == {expected} + seen.clear() + one = discrimalign(A, B, y, num_threads=1, **kwargs) + for key in _param_keys("affine", "general") | set(TRAJECTORY_KEYS): + np.testing.assert_array_equal(np.asarray(auto[key]), np.asarray(one[key]), err_msg=key) + + +@pytest.mark.parametrize("mode", ["local", "global"]) +@pytest.mark.parametrize("gap_mode", ["affine", "linear"]) +@pytest.mark.parametrize("substitution_mode", ["general", "simple"]) +@pytest.mark.parametrize("fill", ["striped", "rowwise", "interpair"]) +def test_nwgrad_alignments_are_the_paths_nwgrad_scored(mode, gap_mode, substitution_mode, fill): + """The returned alignments are nwgrad's own paths: counting them gives exactly the + per-pair counts nwgrad's gradient used at the final parameters, ties included + (simple mode has many), and their scores are nwgrad's scores. This holds for + every fill, so the paths must be traced with the fill the fit used.""" + from src.logit_link import logit_subgradient + from src.nwgrad_backend import NwgradEngine + rng, A, B, y = _data(31, n=16, length=14) + res = discrimalign(A, B, y, aligner_mode=mode, gap_mode=gap_mode, + substitution_mode=substitution_mode, backend="nwgrad", max_iter=3, + stepfunction=create_constant_step(0.01), alphabet="ACGT", + nwgrad_fill=fill) + engine = NwgradEngine(A, B, mode, gap_mode, substitution_mode, "ACGT", 2, fill=fill) + engine.set_params(res) + scores = engine.scores() + counts = engine.count_arrays() + for i, aln in enumerate(res["alignments"]): + assert aln.score == scores[i] + c = logit_subgradient([aln], [0.0], [1], res["alpha"], "ACGT") + np.testing.assert_array_equal(np.asarray(c["Substitutions"]), counts.substitutions[i]) + if gap_mode == "affine": + assert (c["Gap opens"], c["Gap extends"]) == (counts.gap_opens[i], counts.gap_extends[i]) + else: # the linear model reports gap columns only + assert c["Gap opens"] + c["Gap extends"] == counts.gap_opens[i] + counts.gap_extends[i] diff --git a/tests/test_nwgrad_params.py b/tests/test_nwgrad_params.py new file mode 100644 index 0000000..3544e9d --- /dev/null +++ b/tests/test_nwgrad_params.py @@ -0,0 +1,240 @@ +""" +Tests for src.nwgrad_params: parameters converted to nwgrad must reproduce +Biopython's scores, and nwgrad's gradients converted back must reproduce +logit_subgradient's counts. + +The double-precision SeqPairDouble with pointer traceback is used +throughout, so scores are exact up to summation order and paths are optimal. +""" +from copy import deepcopy + +import nwgrad +import numpy as np +import pytest +from Bio.Align import substitution_matrices + +from src.logit_link import logit_subgradient +from src.nwgrad_params import (gap_penalties, grad_to_raw, substitution_data, + to_nwgrad) +from tests.helpers import (DNA, GAP_MODES, MODES, SUBSTITUTION_MODES, align_all, + make_aligner, model_gradient, random_params, small_fixture) + +SEEDS = range(5) + + +def _nwgrad_pair(a, b, mode, gap_mode, substitution_mode, params, alphabet=DNA): + pair = nwgrad.SeqPairDouble(a, b, to_nwgrad(params, gap_mode, substitution_mode, alphabet), + gap_model=gap_mode, mode=mode, traceback="pointers") + pair.score_and_grad() + return pair + + +def _simple(match=2.0, mismatch=-1.0, open_=-3.0, extend=-1.0): + return {"match_score": match, "mismatch_score": mismatch, + "open_gap_score": open_, "extend_gap_score": extend} + + +# --- forward: parameter conversion ------------------------------------------ + +def test_affine_gap_penalties(): + assert gap_penalties({"open_gap_score": -10.0, "extend_gap_score": -0.5}, "affine") == (9.5, 0.5) + + +def test_linear_gap_penalties(): + assert gap_penalties({"gap_score": -6.0}, "linear") == (0.0, 6.0) + + +def test_extend_below_open_gives_negative_surcharge(): + go, ge = gap_penalties({"open_gap_score": -1.0, "extend_gap_score": -10.0}, "affine") + assert (go, ge) == (-9.0, 10.0) + + +def test_simple_mode_matrix(): + data, alphabet = substitution_data({"match_score": 5, "mismatch_score": -4}, "simple", "ACG") + assert alphabet == "ACG" + np.testing.assert_array_equal(data, 9 * np.eye(3) - 4) + assert data.dtype == np.float64 + + +def test_matrix_keeps_its_own_alphabet(): + M = substitution_matrices.Array(alphabet="TGCA", data=np.arange(16.0).reshape(4, 4)) + data, alphabet = substitution_data({"substitution_matrix": M}, "general", "ACGT") + assert alphabet == "TGCA" + np.testing.assert_array_equal(data, np.arange(16.0).reshape(4, 4)) + + +def test_plain_array_matrix_uses_given_alphabet(): + data, alphabet = substitution_data({"substitution_matrix": np.eye(4)}, "general", "ACGT") + assert alphabet == "ACGT" + np.testing.assert_array_equal(data, np.eye(4)) + + +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +def test_to_nwgrad_fields(gap_mode, substitution_mode): + params = random_params(np.random.default_rng(0), gap_mode, substitution_mode) + nw = to_nwgrad(params, gap_mode, substitution_mode, DNA).to_dict() + go, ge = gap_penalties(params, gap_mode) + assert nw["gap_open_a"] == nw["gap_open_b"] == go + assert nw["gap_extend_a"] == nw["gap_extend_b"] == ge + assert nw["alphabet"] == DNA + expected, _ = substitution_data(params, substitution_mode, DNA) + np.testing.assert_array_equal(nw["matrix"], expected) + + +def test_to_nwgrad_does_not_mutate_params(): + params = random_params(np.random.default_rng(1), "affine", "general") + before = deepcopy(params) + to_nwgrad(params, "affine", "general", DNA) + np.testing.assert_array_equal(np.asarray(params["substitution_matrix"]), + np.asarray(before["substitution_matrix"])) + assert params["open_gap_score"] == before["open_gap_score"] + + +@pytest.mark.parametrize("a, b, mode, expected", [ + ("ACGT", "ACGT", "global", 8.0), + ("AGGGC", "AC", "global", 2 + 2 - 3 - 1 - 1), # one gap of length 3 + ("AC", "AGGGC", "global", 2 + 2 - 3 - 1 - 1), # same gap, in the other sequence + ("ACGT", "AGT", "global", 2 + 2 + 2 - 3), # one gap of length 1 + ("TTACGTT", "GGACGGG", "local", 6.0), # ACG + ("AAAA", "CCCC", "local", 0.0), # empty local alignment +]) +def test_hand_computed_scores(a, b, mode, expected): + pair = _nwgrad_pair(a, b, mode, "affine", "simple", _simple()) + assert pair.score == pytest.approx(expected) + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("mode", MODES) +def test_scores_match_biopython(mode, gap_mode, substitution_mode, seed): + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, gap_mode, substitution_mode) + bio = [aln.score for aln in align_all(seqsA, seqsB, make_aligner(mode, params))] + nw = [_nwgrad_pair(a, b, mode, gap_mode, substitution_mode, params).score + for a, b in zip(seqsA, seqsB)] + assert nw == pytest.approx(bio, rel=1e-12, abs=1e-12) + + +@pytest.mark.parametrize("a, b", [("A", "C"), ("AT", "CG"), ("GAT", "GCT"), ("AB", "CD")]) +def test_scores_match_biopython_when_gaps_beat_mismatches(a, b): + """Adjacent gaps in different sequences, and extend below open.""" + for params in (_simple(1.0, -100.0, -1.0, -0.5), _simple(1.0, -100.0, -1.0, -10.0)): + bio = align_all([a], [b], make_aligner("global", params))[0].score + assert _nwgrad_pair(a, b, "global", "affine", "simple", params, "ABCDGT").score == \ + pytest.approx(bio) + + +# --- backward: gradient conversion ------------------------------------------ + +def _raw_counts(aln, alphabet=DNA): + return logit_subgradient([aln], [0.0], [1], 0.0, alphabet) + + +def test_grad_to_raw_gap_of_length_three(): + raw = grad_to_raw(_nwgrad_pair("AGGGC", "AC", "global", "affine", "simple", _simple()).grad) + assert raw["Gap opens"] == 1 + assert raw["Gap extends"] == 2 + assert raw["Substitutions"]["A", "A"] == 1 + assert raw["Substitutions"]["C", "C"] == 1 + assert np.asarray(raw["Substitutions"]).sum() == 2 + + +def test_grad_to_raw_alternating_gaps_are_all_opens(): + params = _simple(1.0, -100.0, -1.0, -10.0) + raw = grad_to_raw(_nwgrad_pair("AB", "CD", "global", "affine", "simple", params, "ABCD").grad) + assert raw["Gap opens"] == 4 + assert raw["Gap extends"] == 0 + + +def test_grad_to_raw_substitution_orientation(): + """Rows are residues of sequence A, columns residues of sequence B.""" + params = {"substitution_matrix": substitution_matrices.Array( + alphabet=DNA, data=np.where(np.eye(4) > 0, 2.0, -1.0)), + "open_gap_score": -10.0, "extend_gap_score": -10.0} + raw = grad_to_raw(_nwgrad_pair("AG", "CT", "global", "affine", "general", params).grad) + assert raw["Substitutions"]["A", "C"] == 1 + assert raw["Substitutions"]["C", "A"] == 0 + assert raw["Substitutions"]["G", "T"] == 1 + + +def test_grad_to_raw_accepts_dict_and_object(): + grad = _nwgrad_pair("ACGGT", "AT", "global", "affine", "simple", _simple()).grad + a, b = grad_to_raw(grad), grad_to_raw(grad.to_dict()) + np.testing.assert_array_equal(np.asarray(a["Substitutions"]), np.asarray(b["Substitutions"])) + assert (a["Gap opens"], a["Gap extends"]) == (b["Gap opens"], b["Gap extends"]) + + +def test_grad_to_raw_is_linear(): + """Weighted sums of gradients may be converted after summing.""" + params = _simple() + grads = [_nwgrad_pair(a, b, "global", "affine", "simple", params).grad.to_dict() + for a, b in [("AGGGC", "AC"), ("ACGT", "AGT"), ("GATTACA", "GACA")]] + weights = [0.3, -1.2, 0.7] + summed = {k: sum(w * g[k] for w, g in zip(weights, grads)) + for k in ("matrix", "gap_open_a", "gap_extend_a", "gap_open_b", "gap_extend_b")} + summed["alphabet"] = grads[0]["alphabet"] + raw_sum = grad_to_raw(summed) + parts = [grad_to_raw(g) for g in grads] + np.testing.assert_allclose(np.asarray(raw_sum["Substitutions"]), + sum(w * np.asarray(p["Substitutions"]) for w, p in zip(weights, parts))) + for key in ("Gap opens", "Gap extends"): + assert raw_sum[key] == pytest.approx(sum(w * p[key] for w, p in zip(weights, parts))) + + +def test_empty_local_alignment_gradient_is_zero(): + raw = grad_to_raw(_nwgrad_pair("AAAA", "CCCC", "local", "affine", "simple", _simple()).grad) + assert np.all(np.asarray(raw["Substitutions"]) == 0) + assert raw["Gap opens"] == 0 + assert raw["Gap extends"] == 0 + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("substitution_mode", SUBSTITUTION_MODES) +@pytest.mark.parametrize("gap_mode", GAP_MODES) +@pytest.mark.parametrize("mode", MODES) +def test_model_gradient_matches_logit_subgradient(mode, gap_mode, substitution_mode, seed): + """ + Per pair, the gradient in the model's own parameters agrees. + + Raw counts can legitimately differ where the model has ties: in simple + mode an A/C and an A/G mismatch score the same, and in linear mode so + do different gap placements. Only the model's parameters must agree. + """ + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, gap_mode, substitution_mode) + alns = align_all(seqsA, seqsB, make_aligner(mode, params)) + for a, b, aln in zip(seqsA, seqsB, alns): + nw = model_gradient(grad_to_raw(_nwgrad_pair(a, b, mode, gap_mode, substitution_mode, + params).grad), gap_mode, substitution_mode) + bio = model_gradient(_raw_counts(aln), gap_mode, substitution_mode) + assert nw.keys() == bio.keys() + for key in nw: + np.testing.assert_array_equal(np.asarray(nw[key]), np.asarray(bio[key]), + err_msg=f"{key}: {a} / {b}") + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("mode", MODES) +def test_raw_counts_match_logit_subgradient_without_ties(mode, seed): + """General matrix and affine gaps at continuous parameters: the path is unique.""" + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, "affine", "general") + alns = align_all(seqsA, seqsB, make_aligner(mode, params)) + for a, b, aln in zip(seqsA, seqsB, alns): + nw = grad_to_raw(_nwgrad_pair(a, b, mode, "affine", "general", params).grad) + bio = _raw_counts(aln) + np.testing.assert_array_equal(np.asarray(nw["Substitutions"]), + np.asarray(bio["Substitutions"]), err_msg=f"{a} / {b}") + assert (nw["Gap opens"], nw["Gap extends"]) == (bio["Gap opens"], bio["Gap extends"]) + + +@pytest.mark.parametrize("seed", SEEDS) +@pytest.mark.parametrize("mode", MODES) +def test_linear_mode_reports_all_gap_columns_as_extends(mode, seed): + rng, seqsA, seqsB, _ = small_fixture(seed) + params = random_params(rng, "linear", "simple") + for a, b in zip(seqsA, seqsB): + nw = grad_to_raw(_nwgrad_pair(a, b, mode, "linear", "simple", params).grad) + assert nw["Gap opens"] == 0 diff --git a/tests/test_optimization.py b/tests/test_optimization.py new file mode 100644 index 0000000..eff3edd --- /dev/null +++ b/tests/test_optimization.py @@ -0,0 +1,406 @@ +"""Unit tests for src.optimization.""" +import warnings + +import numpy as np +import pytest +from Bio.Align import PairwiseAligner, substitution_matrices +from joblib import Parallel +from scipy.special import expit + +from src.optimization import (EmptyAlignmentCounts, EmptyLocalAlignment, + _count_arrays_from_raw, _count_features, _fit_initial_estimate, + create_alignment_workers, create_constant_step, + create_powerstep, get_first_alignment, + get_initial_estimate, get_initial_estimate_from_counts) +from tests.helpers import DNA, align_all, make_aligner, make_pairs, random_params + + +# --- EmptyLocalAlignment ---------------------------------------------------- + +def test_empty_local_alignment_score_is_zero(): + assert EmptyLocalAlignment().score == 0.0 + + +def test_empty_local_alignment_counts_are_zero(): + counts = EmptyLocalAlignment().counts() + for field in ("identities", "mismatches", "open_gaps", "extend_gaps", "gaps"): + assert getattr(counts, field) == 0 + + +def test_empty_local_alignment_rows_are_empty_strings(): + aln = EmptyLocalAlignment() + assert aln[0] == "" + assert aln[1] == "" + + +@pytest.mark.parametrize("index", [2, -1, 5]) +def test_empty_local_alignment_other_rows_raise(index): + with pytest.raises(IndexError): + EmptyLocalAlignment()[index] + + +def test_empty_alignment_counts_class_attributes(): + assert EmptyAlignmentCounts.gaps == 0 + + +# --- get_first_alignment ---------------------------------------------------- + +def _simple_aligner(mode): + aligner = PairwiseAligner() + aligner.mode = mode + aligner.match_score = 5 + aligner.mismatch_score = -4 + aligner.open_gap_score = -8 + aligner.extend_gap_score = -0.5 + return aligner + + +def test_first_alignment_global(): + aln = get_first_alignment("ACGT", "ACGT", _simple_aligner("global")) + assert aln.score == 20 + assert aln[0] == "ACGT" + + +def test_first_alignment_local_substring(): + aln = get_first_alignment("TTTACGTTTT", "GGACGGG", _simple_aligner("local")) + assert aln.score == 15 # ACG + + +@pytest.mark.parametrize("a, b", [("AAAA", "CCCC"), ("A", "C"), ("ACAC", "GTGT")]) +def test_first_alignment_local_without_positive_score_is_empty(a, b): + aln = get_first_alignment(a, b, _simple_aligner("local")) + assert isinstance(aln, EmptyLocalAlignment) + assert aln.score == 0.0 + + +def test_first_alignment_global_always_exists_for_unrelated_sequences(): + aln = get_first_alignment("AAAA", "CCCC", _simple_aligner("global")) + assert aln.score < 0 + + +def test_first_alignment_reraises_stopiteration_outside_local_mode(): + class NoAlignments: + mode = "global" + + def align(self, a, b): + return iter(()) + + with pytest.raises(StopIteration): + get_first_alignment("A", "C", NoAlignments()) + + +# --- create_alignment_workers ----------------------------------------------- + +@pytest.mark.parametrize("mode", ["local", "global"]) +def test_alignment_workers_reproduce_serial_alignments(mode): + rng = np.random.default_rng(1) + seqsA, seqsB, _ = make_pairs(rng, 4, 4, 12) + aligner = _simple_aligner(mode) + parallel = Parallel(n_jobs=2, prefer="threads") + alns = parallel(create_alignment_workers(seqsA, seqsB, aligner)) + expected = align_all(seqsA, seqsB, aligner) + assert [a.score for a in alns] == [e.score for e in expected] + + +def test_alignment_workers_is_lazy_and_sized_by_input(): + gen = create_alignment_workers(["A", "C", "G"], ["A", "C", "G"], _simple_aligner("global")) + assert len(list(gen)) == 3 + + +# --- step functions --------------------------------------------------------- + +@pytest.mark.parametrize("scale", [0.0, 0.01, 3.5]) +def test_constant_step(scale): + step = create_constant_step(scale) + assert [step(i) for i in range(5)] == [scale] * 5 + + +def test_powerstep_default_is_inverse_square_root(): + step = create_powerstep(2.0) + for i in range(10): + assert step(i) == pytest.approx(2.0 / np.sqrt(i + 1)) + + +@pytest.mark.parametrize("power", [0.0, 0.5, 1.0, 2.0]) +def test_powerstep_power(power): + step = create_powerstep(1.0, power=power) + assert step(3) == pytest.approx(4.0 ** -power) + + +def test_powerstep_burnin_is_constant_then_decays_from_scale(): + step = create_powerstep(1.0, power=1.0, burnin=3) + assert [step(i) for i in range(3)] == [1.0, 1.0, 1.0] + assert step(3) == pytest.approx(1.0) # first post-burnin step is still the full scale + assert step(4) == pytest.approx(0.5) + assert step(12) == pytest.approx(0.1) + + +def test_powerstep_is_nonincreasing(): + step = create_powerstep(0.3, power=0.7, burnin=2) + values = [step(i) for i in range(50)] + assert all(a >= b for a, b in zip(values, values[1:])) + + +# --- initial estimate on synthetic count features --------------------------- +# +# Fake alignments with prescribed count features, labelled by a known logistic +# model. Fitting recovers the model, which pins down the mapping from +# regression coefficients to parameter names. + +class FakeCounts: + def __init__(self, identities, mismatches, open_gaps, extend_gaps): + self.identities = identities + self.mismatches = mismatches + self.open_gaps = open_gaps + self.extend_gaps = extend_gaps + self.gaps = open_gaps + extend_gaps + + +class FakeAlignment: + def __init__(self, row0, row1, counts): + self.rows = (row0, row1) + self._counts = counts + + def counts(self): + return self._counts + + def __getitem__(self, index): + return self.rows[index] + + +def _fake_simple_data(rng, n, coefs, alpha, linear=False): + alns, labels = [], [] + for _ in range(n): + ident, mism = rng.poisson(4), rng.poisson(3) + opens, extends = rng.poisson(1.0), rng.poisson(1.5) + if linear: + x = np.array([ident, mism, opens + extends]) + else: + x = np.array([ident, mism, opens, extends]) + labels.append(int(rng.random() < expit(alpha + x @ coefs))) + alns.append(FakeAlignment("", "", FakeCounts(ident, mism, opens, extends))) + return alns, np.array(labels) + + +def test_initial_estimate_simple_affine_recovers_coefficients(): + rng = np.random.default_rng(0) + coefs = np.array([0.8, -0.6, -1.0, -0.3]) + alns, labels = _fake_simple_data(rng, 4000, coefs, alpha=-0.5) + est = get_initial_estimate(alns, labels, "simple", "affine") + assert set(est) == {"alpha", "match_score", "mismatch_score", + "open_gap_score", "extend_gap_score"} + assert est["alpha"] == pytest.approx(-0.5, abs=0.25) + assert est["match_score"] == pytest.approx(0.8, abs=0.1) + assert est["mismatch_score"] == pytest.approx(-0.6, abs=0.1) + assert est["open_gap_score"] == pytest.approx(-1.0, abs=0.15) + assert est["extend_gap_score"] == pytest.approx(-0.3, abs=0.1) + + +def test_initial_estimate_simple_linear_recovers_coefficients(): + rng = np.random.default_rng(1) + coefs = np.array([0.8, -0.6, -0.7]) + alns, labels = _fake_simple_data(rng, 4000, coefs, alpha=-0.5, linear=True) + est = get_initial_estimate(alns, labels, "simple", "linear") + assert set(est) == {"alpha", "match_score", "mismatch_score", "gap_score"} + assert est["match_score"] == pytest.approx(0.8, abs=0.1) + assert est["mismatch_score"] == pytest.approx(-0.6, abs=0.1) + assert est["gap_score"] == pytest.approx(-0.7, abs=0.1) + + +def _fake_full_data(rng, n, matrix, open_coef, extend_coef, alpha, alphabet): + alns, labels = [], [] + k = len(alphabet) + for _ in range(n): + pair_counts = rng.poisson(0.6, size=(k, k)) + opens, extends = rng.poisson(1.0), rng.poisson(1.5) + row0 = "".join(alphabet[i] * pair_counts[i, j] for i in range(k) for j in range(k)) + row1 = "".join(alphabet[j] * pair_counts[i, j] for i in range(k) for j in range(k)) + # Gap columns in the rows must be ignored by the substitution features. + row0 += "-" * opens + row1 += alphabet[0] * opens + eta = alpha + np.sum(pair_counts * matrix) + opens * open_coef + extends * extend_coef + labels.append(int(rng.random() < expit(eta))) + ident = int(np.trace(pair_counts)) + alns.append(FakeAlignment(row0, row1, + FakeCounts(ident, int(pair_counts.sum()) - ident, opens, extends))) + return alns, np.array(labels) + + +def test_initial_estimate_general_affine_matrix_orientation(): + rng = np.random.default_rng(2) + alphabet = "ACG" + matrix = np.full((3, 3), -0.3) + matrix[np.diag_indices(3)] = 0.6 + matrix[0, 1] = 1.2 # A->C strongly positive, C->A not + alns, labels = _fake_full_data(rng, 4000, matrix, -0.8, -0.2, -0.3, alphabet) + est = get_initial_estimate(alns, labels, "general", "affine", alphabet) + assert set(est) == {"alpha", "substitution_matrix", "open_gap_score", "extend_gap_score"} + M = est["substitution_matrix"] + assert "".join(M.alphabet) == alphabet + # Default L2 penalty shrinks the fit a little, so the tolerance is loose. + np.testing.assert_allclose(np.asarray(M), matrix, atol=0.2) + assert M["A", "C"] > M["C", "A"] + 0.8 + assert est["open_gap_score"] == pytest.approx(-0.8, abs=0.2) + assert est["extend_gap_score"] == pytest.approx(-0.2, abs=0.15) + + +def test_initial_estimate_symmetric_is_symmetrized_general(): + rng = np.random.default_rng(3) + alphabet = "ACG" + matrix = np.full((3, 3), -0.3) + matrix[np.diag_indices(3)] = 0.6 + matrix[0, 1] = 1.2 + alns, labels = _fake_full_data(rng, 1000, matrix, -0.8, -0.2, -0.3, alphabet) + general = get_initial_estimate(alns, labels, "general", "affine", alphabet) + symmetric = get_initial_estimate(alns, labels, "symmetric", "affine", alphabet) + G = np.asarray(general["substitution_matrix"]) + S = symmetric["substitution_matrix"] + np.testing.assert_allclose(np.asarray(S), (G + G.T) / 2) + assert "".join(S.alphabet) == alphabet + assert symmetric["open_gap_score"] == pytest.approx(general["open_gap_score"]) + + +@pytest.mark.parametrize("substitution_mode", ["general", "symmetric"]) +def test_initial_estimate_full_linear_fits_one_gap_coefficient(substitution_mode): + """Labels from a linear gap model: every gap column has the same coefficient.""" + rng = np.random.default_rng(4) + gap = -0.6 + alns, labels = _fake_full_data(rng, 4000, np.eye(3) - 0.3, gap, gap, -0.3, "ACG") + est = get_initial_estimate(alns, labels, substitution_mode, "linear", "ACG") + assert set(est) == {"alpha", "substitution_matrix", "gap_score"} + # The former open + extend of an affine fit would land near 2 * gap. + assert est["gap_score"] == pytest.approx(gap, abs=0.1) + + +def test_initial_estimate_rejects_unknown_modes(): + alns = [FakeAlignment("", "", FakeCounts(1, 0, 0, 0))] * 2 + with pytest.raises(AssertionError): + get_initial_estimate(alns, [0, 1], "simple", "convex") + with pytest.raises(AssertionError): + get_initial_estimate(alns, [0, 1], "banded", "affine") + + +@pytest.mark.parametrize("substitution_mode", ["general", "symmetric"]) +def test_initial_estimate_full_requires_alphabet(substitution_mode): + alns = [FakeAlignment("", "", FakeCounts(1, 0, 0, 0))] * 2 + with pytest.raises(AssertionError): + get_initial_estimate(alns, [0, 1], substitution_mode, "affine") + + +def test_initial_estimate_needs_both_classes(): + alns = [FakeAlignment("", "", FakeCounts(i, 1, 0, 0)) for i in range(4)] + with pytest.raises(ValueError): + get_initial_estimate(alns, [1, 1, 1, 1], "simple", "affine") + + +# --- initial estimate on real alignments ------------------------------------ + +@pytest.mark.parametrize("mode", ["local", "global"]) +@pytest.mark.parametrize("gap_mode", ["affine", "linear"]) +@pytest.mark.parametrize("substitution_mode", ["simple", "symmetric", "general"]) +def test_initial_estimate_on_real_alignments(mode, gap_mode, substitution_mode): + rng = np.random.default_rng(5) + seqsA, seqsB, labels = make_pairs(rng, 20, 20, 25, sub_rate=0.2) + params = random_params(rng, gap_mode, substitution_mode) + alns = align_all(seqsA, seqsB, make_aligner(mode, params)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # convergence warnings on tiny data + est = get_initial_estimate(alns, labels, substitution_mode, gap_mode, DNA) + for key, value in est.items(): + assert np.all(np.isfinite(np.asarray(value))), key + if substitution_mode == "simple": + assert est["match_score"] > est["mismatch_score"] + else: + M = np.asarray(est["substitution_matrix"]) + assert np.mean(np.diag(M)) > np.mean(M[~np.eye(4, dtype=bool)]) + if substitution_mode == "symmetric": + np.testing.assert_allclose(M, M.T) + + +def test_initial_estimate_accepts_empty_local_alignments(): + alns = [EmptyLocalAlignment(), EmptyLocalAlignment(), + FakeAlignment("AC", "AC", FakeCounts(2, 0, 0, 0)), + FakeAlignment("AG", "AG", FakeCounts(2, 0, 0, 0))] + labels = [0, 0, 1, 1] + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + est = get_initial_estimate(alns, labels, "general", "affine", "ACGT") + assert np.all(np.isfinite(np.asarray(est["substitution_matrix"]))) + + +# --- count features ---------------------------------------------------------- + +def _reference_count_features(raw_counts_list, substitution_mode, gap_mode, alphabet): + """The per-row implementation that _count_features() replaced.""" + predictors = [] + for raw in raw_counts_list: + subs = raw['Substitutions'] + G = np.asarray(subs) + opens, extends = raw['Gap opens'], raw['Gap extends'] + gaps = [opens, extends] if gap_mode == 'affine' else [opens + extends] + if substitution_mode == 'simple': + predictors.append([np.trace(G), G.sum() - np.trace(G)] + gaps) + else: + order = [subs.alphabet.index(char) for char in alphabet] + predictors.append(list(G[np.ix_(order, order)].ravel()) + gaps) + return predictors + + +def _random_raw_counts(rng, n, count_alphabet): + """Counts as grad_to_raw() returns them, over count_alphabet.""" + k = len(count_alphabet) + return [{'Substitutions': substitution_matrices.Array( + alphabet=count_alphabet, data=rng.poisson(1.5, (k, k)).astype(float)), + 'Gap opens': float(rng.poisson(1)), + 'Gap extends': float(rng.poisson(2))} for _ in range(n)] + + +# Counts over the fit's alphabet, a permutation of it, and a superset of it. +COUNT_ALPHABETS = [DNA, "TGCA", "ACGTN"] + + +@pytest.mark.parametrize("count_alphabet", COUNT_ALPHABETS) +@pytest.mark.parametrize("gap_mode", ["affine", "linear"]) +@pytest.mark.parametrize("substitution_mode", ["simple", "symmetric", "general"]) +def test_count_features_match_per_row_reference(substitution_mode, gap_mode, count_alphabet): + raws = _random_raw_counts(np.random.default_rng(0), 40, count_alphabet) + expected = np.asarray(_reference_count_features(raws, substitution_mode, gap_mode, DNA), + dtype=float) + for counts in (raws, _count_arrays_from_raw(raws)): + features = _count_features(counts, substitution_mode, gap_mode, DNA) + assert features.dtype == np.float64 + np.testing.assert_array_equal(features, expected) + + +def test_count_arrays_from_raw_reorders_to_the_first_alphabet(): + rng = np.random.default_rng(1) + raws = _random_raw_counts(rng, 3, DNA) + _random_raw_counts(rng, 2, "TGCA") + counts = _count_arrays_from_raw(raws) + assert counts.alphabet == DNA + for i, raw in enumerate(raws): + for a, x in enumerate(DNA): + for b, z in enumerate(DNA): + assert counts.substitutions[i, a, b] == raw['Substitutions'][x, z] + np.testing.assert_array_equal(counts.gap_opens, [r['Gap opens'] for r in raws]) + np.testing.assert_array_equal(counts.gap_extends, [r['Gap extends'] for r in raws]) + + +@pytest.mark.parametrize("gap_mode", ["affine", "linear"]) +@pytest.mark.parametrize("substitution_mode", ["simple", "symmetric", "general"]) +def test_initial_estimate_from_counts_is_unchanged(substitution_mode, gap_mode): + rng = np.random.default_rng(2) + raws = _random_raw_counts(rng, 60, "TGCA") + labels = rng.integers(0, 2, len(raws)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + reference = _fit_initial_estimate( + _reference_count_features(raws, substitution_mode, gap_mode, DNA), + labels, substitution_mode, gap_mode, DNA) + from_list = get_initial_estimate_from_counts(raws, labels, substitution_mode, gap_mode, DNA) + from_arrays = get_initial_estimate_from_counts(_count_arrays_from_raw(raws), labels, + substitution_mode, gap_mode, DNA) + for estimate in (from_list, from_arrays): + assert estimate.keys() == reference.keys() + for key in reference: + np.testing.assert_array_equal(np.asarray(estimate[key]), np.asarray(reference[key]), + err_msg=key)