Skip to content

Commit dc11b5b

Browse files
authored
Merge pull request #17 from platforma-open/feat/seq-properties-vectorization
Vectorize compute-properties: replace per-row BioPython with array math (~10x at scale)
2 parents 71a3e1f + 36e7aab commit dc11b5b

56 files changed

Lines changed: 4630 additions & 348 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@platforma-open/milaboratories.sequence-properties.software': patch
3+
'@platforma-open/milaboratories.sequence-properties.workflow': patch
4+
---
5+
6+
Vectorize the property computation — single-threaded numpy/polars array math replaces per-row BioPython. ~5x faster at 50k clones; output unchanged within the quantized-equal contract (signed-zero canonicalized).
7+
8+
Reduce the vectorized engine's peak memory (int8/int32 indices, share the clean intermediate between counts and instability, drop dead retention) — ~4.85 GB/1M clones vs ~8.5 GB before. Raise the compute-properties step's `mem()` from 4GiB to 16GiB to cover ~2M clones at the new footprint with headroom.

.github/workflows/python-tests.yaml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323

2424
- uses: astral-sh/setup-uv@v5
2525
with:
26+
version: "0.11.16"
2627
enable-cache: true
2728
cache-dependency-glob: software/uv.lock
2829

@@ -36,4 +37,43 @@ jobs:
3637
run: uv run ruff format --check
3738

3839
- name: Run pytest
39-
run: uv run pytest
40+
# Exclude @slow (the perf benchmark) from the gate — it asserts a
41+
# wall-clock budget that flakes on shared runners. Run it explicitly
42+
# with `uv run pytest -m slow`.
43+
run: uv run pytest -m "not slow"
44+
45+
requirements-sync:
46+
# src/requirements.txt is generated from pyproject.toml (the single source of
47+
# truth) and is what pl-pkg builds from. This job fails if the committed file
48+
# has drifted from pyproject — e.g. someone changed a dependency but forgot to
49+
# regenerate. Fix locally with `pnpm deps:export` (see software/package.json).
50+
if: github.repository != 'milaboratory/platforma-sequence-properties'
51+
runs-on: ubuntu-latest
52+
defaults:
53+
run:
54+
working-directory: software
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- uses: astral-sh/setup-uv@v5
59+
with:
60+
version: "0.11.16"
61+
enable-cache: true
62+
cache-dependency-glob: software/uv.lock
63+
64+
- name: Regenerate requirements.txt from pyproject.toml
65+
# Same script `pnpm deps:export` runs locally — single source of truth.
66+
run: bash scripts/deps-export.sh
67+
68+
- name: Verify requirements.txt is in sync with pyproject.toml
69+
run: |
70+
if ! git diff --exit-code -- src/requirements.txt; then
71+
echo ""
72+
echo "::error::src/requirements.txt is out of sync with pyproject.toml"
73+
echo "Dependencies are defined in software/pyproject.toml; src/requirements.txt is generated."
74+
echo "Resolve locally with:"
75+
echo " cd software && pnpm deps:export"
76+
echo "then commit the updated src/requirements.txt."
77+
exit 1
78+
fi
79+
echo "src/requirements.txt is in sync with pyproject.toml."

software/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"changeset": "changeset",
99
"version-packages": "changeset version",
1010
"build": "pl-pkg build",
11+
"deps:export": "bash scripts/deps-export.sh",
1112
"prepublishOnly": "pl-pkg prepublish"
1213
},
1314
"block-software": {

software/pyproject.toml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@
22
name = "sequence-properties-calc-script"
33
version = "0.0.0"
44
requires-python = ">=3.12.0, <3.13"
5+
# Runtime dependencies — single source of truth. These exact pins are exported to
6+
# src/requirements.txt (top-level only) via `pnpm deps:export`, which is what pl-pkg
7+
# builds from and what the scientific-slim runenv installs offline. Keep versions
8+
# aligned with the wheels shipped by the runenv.
9+
dependencies = [
10+
"polars-lts-cpu==1.33.1",
11+
"numpy==2.2.6",
12+
"pyarrow==21.0.0",
13+
"biopython==1.87",
14+
]
515

616
[dependency-groups]
17+
# Dev-only tooling (not shipped). Runtime libs live in [project.dependencies] above.
718
dev = [
8-
"polars>=1.39.0",
9-
"numpy>=2.0.0",
10-
"pyarrow>=18.0.0",
11-
"biopython>=1.84",
1219
"pytest>=9.0.2",
1320
"pytest-cov>=6.0.0",
1421
"ruff>=0.7.0",
22+
"hypothesis>=6.155.2",
1523
]
1624

1725
[tool.pytest.ini_options]
@@ -51,6 +59,13 @@ ignore = []
5159
fixable = ["ALL"]
5260
unfixable = []
5361

62+
[tool.ruff.lint.per-file-ignores]
63+
# E402: these modules intentionally run setup before imports — main.py pins
64+
# POLARS_MAX_THREADS before polars is imported; _corpus_gen.py adds src to
65+
# sys.path before importing the package (it runs standalone via `python -m`).
66+
"src/main.py" = ["E402"]
67+
"tests/_corpus_gen.py" = ["E402"]
68+
5469
[tool.ruff.format]
5570
quote-style = "double"
5671
indent-style = "space"

software/scripts/deps-export.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
# Regenerate src/requirements.txt from pyproject.toml (the single source of truth).
3+
# One definition, shared by `pnpm deps:export` and the requirements-sync CI job.
4+
#
5+
# --no-deps: top-level pins only. The runenv supplies transitive deps for the
6+
# offline (--no-index) install, so requirements.txt must not pin a full closure.
7+
set -euo pipefail
8+
cd "$(dirname "$0")/.."
9+
uv pip compile pyproject.toml --no-deps --no-annotate \
10+
--custom-compile-command "pnpm deps:export" -o src/requirements.txt

software/src/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99

1010
from __future__ import annotations
1111

12+
import os
13+
14+
# Pin the polars thread pool to 1 BEFORE polars (or anything importing it,
15+
# including `pipeline`) is loaded — POLARS_MAX_THREADS is read once at import.
16+
# The block reserves cpu(1), and single-threaded execution keeps output
17+
# byte-stable (no thread-count-dependent reduction order). setdefault so an
18+
# explicit override from the environment still wins.
19+
os.environ.setdefault("POLARS_MAX_THREADS", "1")
20+
1221
import argparse
1322
import json
1423
import logging

0 commit comments

Comments
 (0)