Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Run tests

on:
pull_request:
branches:
- main
push:
branches:
- main

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
test:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- '3.11'
- '3.12'
- '3.13'
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: ${{ matrix.python-version }}

# The pileup extra resolves giql from a git revision, so the
# install needs network here. See the comment on
# `[tool.uv.sources]` in pyproject.toml.
#
# `--locked` so a green run says the committed lock resolves,
# rather than that some resolution uv wrote and discarded did.
- name: Install
run: uv sync --locked --all-extras

- name: Lint
run: |
uv run ruff format --check src tests
uv run ruff check src tests

- name: Test
run: uv run pytest -q

base-install:
name: Base install
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: '3.11'

# No extras and no dev group, which is the install the README and
# the package docstring call sufficient. It is a job of its own
# because syncing without extras would strip the venv the test job
# needs.
- name: Install
run: uv sync --locked --no-dev

- name: Import
run: uv run --no-sync python -c "import quadrat; print(quadrat.__version__)"
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# macOS
.DS_Store

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
build/
dist/
sdist/
wheels/
*.egg-info/
*.egg
.eggs/

# Environments
.venv/
venv/

# Tooling
.pytest_cache/
.ruff_cache/
.hypothesis/
.coverage
coverage.xml
htmlcov/

# Docs
docs/_build/

# Jupyter
.ipynb_checkpoints/

# SDLC working state: local review roles, guides and accumulated review
# documents. Untracked here as it is in the sibling projects -- the
# review history is versioned by its own repo under .sdlc, and the
# guides are local working config rather than a project artifact.
.sdlc/
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# quadrat

Sequence-to-function training data, straight from BAM.

A quadrat is the fixed-size frame a field ecologist drops on the ground to tally everything inside it, moved across a landscape by a sampling design chosen separately from the frame. This package is that: a fixed-length window, placed at loci by a sampling scheme, tallying the cut sites that fall inside.

Everything between a set of alignments and a batch of `(sequence, signal)` pairs is independent of the model that consumes them. `quadrat` implements that span — the pileup, the window cache, the sampler and the loader — so any sequence-to-function architecture can be trained without reimplementing it.

## Status

Under construction. The data path is being extracted from [cherimoya](https://github.com/conradbzura/cherimoya); see [#1](https://github.com/abdenlab/quadrat/issues/1) for the specification and the slices it lands in.

## Install

Nothing is published yet. Once the data path lands, the two installs will be:

```bash
pip install quadrat # stream windows over cut-site tables
pip install "quadrat[pileup]" # also build those tables from alignments
```

The `pileup` extra adds the engine that builds those tables: [polars-bio](https://github.com/biodatageeks/polars-bio) and [giql](https://github.com/abdenlab/giql) among its dependencies. The base install is enough for a caller who already has cut-site tables; `quadrat`'s package docstring states the import guarantee that makes it so.

The extra resolves with `uv` and not yet with pip, because one of its dependencies is pinned to a git revision that only `uv` reads — the comment on `[tool.uv.sources]` in `pyproject.toml` says why and when that changes.

## Development

```bash
uv sync --locked --all-extras # install, pileup engine included
uv run pytest -q # run the suite
uv run ruff format --check src tests # check formatting
uv run ruff check src tests # lint
```

`uv sync --locked --no-dev` installs what `pip install quadrat` will, which is the base install the import guarantee above is about; CI runs it as a job of its own.

## License

MIT.
141 changes: 141 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]

[project]
authors = [
{ name = "Nezar Abdennur", email = "nabdennur@gmail.com" },
{ name = "Conrad Bzura", email = "conradbzura@gmail.com" },
]
# No licence classifier: PEP 639 deprecates them and directs tools not
# to combine one with `License-Expression`, which `license = "MIT"`
# below produces. PyPI rejects a distribution carrying both.
classifiers = [
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Bio-Informatics",
]
description = "Sequence-to-function training data, straight from BAM."
dynamic = ["version"]
license = "MIT"
maintainers = [
{ name = "Nezar Abdennur", email = "nabdennur@gmail.com" },
{ name = "Conrad Bzura", email = "conradbzura@gmail.com" },
]
name = "quadrat"
readme = "README.md"
requires-python = ">=3.11"

# Floors, not tested ceilings. 3.11 rather than 3.10 because the pileup
# engine -- polars-bio, polars, giql -- is gated there, and the pileup is
# not an optional part of what this package is for.
dependencies = [
"numpy>=1.14.2",
"pandas>=1.3.3",
"torch>=2.9.0",
"tqdm>=4.64.1",
"joblib>=1.0.0",
"tangermeme>=1.5.0",
]

# `pyfaidx` belongs in this extra rather than in a dev group: the
# pileup imports it at runtime, so it is a runtime dependency of the
# extra rather than a test one. The lazy-import guarantee that makes
# the base install sufficient is stated in the package docstring.
[project.optional-dependencies]
pileup = [
"polars-bio>=0.35.1",
"polars>=1.30",
"giql>=0.7",
"pyfaidx>=0.9",
]

[dependency-groups]
dev = [
"pytest>=9.1.1",
"pytest-mock>=3.14",
"hypothesis>=6.0.0",
"pysam",
"ruff>=0.1.0",
]
docs = [
"sphinx>=7.0",
"sphinx-autobuild>=2024.0",
"sphinx-book-theme>=1.1",
"sphinx-design>=0.6",
]

# BRANCH-ONLY. The datafusion-bio GIQL target is not on PyPI and lives on
# a proof-of-concept branch, so this pins the public commit at that
# branch's tip. Its library code is identical to what cherimoya builds
# against; the only delta is benchmark material outside `src`. Replace
# with a version specifier once the target reaches `main` and ships.
[tool.uv.sources]
giql = { git = "https://github.com/abdenlab/giql", rev = "59c74276e07cc4680cb24303a9192c3a71c11d59" }

[tool.hatch.version]
path = "src/quadrat/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/quadrat"]

# The whole test tree ships, so the suite runs from an sdist exactly as
# it does from the repository -- without conftest.py it would run
# against a different cache directory than the one the suite assumes.
#
# Stated here rather than in a MANIFEST.in, which hatchling does not
# read: a MANIFEST.in's directives are inert under this backend, and an
# sdist built with or without the file is byte-identical.
[tool.hatch.build.targets.sdist]
include = [
"src",
"tests",
"README.md",
"LICENSE",
]

# `**` on both sides, because this is a gitignore-style pattern: one
# containing a slash is anchored at the project root and a single `*`
# spans one path segment, so `*/.ipynb_checkpoints/*` excludes a
# checkpoint directory one level down and ships every deeper one.
exclude = ["**/.ipynb_checkpoints/**"]

[tool.ruff]
line-length = 89

# Stated explicitly rather than left to the default, which has widened
# over ruff releases and would otherwise change what passes when the
# pin moves. Pyflakes plus the pycodestyle errors that are errors:
# undefined names, unused imports, broken syntax.
#
# Deliberately absent are the style-migration families -- UP, SIM, FURB,
# RUF. The code ported from cherimoya uses `str.format` throughout and
# `class X(object)` by convention. Flagging either would mean rewriting
# thousands of lines in the same change that moves them, which is the
# one thing that makes a port unreviewable.
#
# `I` is in despite that argument, because import order is mechanical in
# ported code too, `--fix` for it is safe and localised, and getting it
# wrong is the one style defect a reviewer reliably misses.
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I"]

[tool.ruff.format]
docstring-code-format = true

# `--strict-markers` turns a misspelt marker into an error rather than a
# test that is silently neither selected nor deselected.
[tool.pytest.ini_options]
addopts = ["--strict-markers"]
testpaths = ["tests"]
markers = [
"packaging: spawns a fresh interpreter or a real build to observe import-time or artifact state",
]

# Scoped rather than blanket: a deprecation raised inside `quadrat` is a
# stale API this suite should fail on, while pysam deprecates ahead of
# removals on a schedule this project does not set.
filterwarnings = [
"error::DeprecationWarning:quadrat.*",
"ignore::DeprecationWarning:pysam.*",
]
43 changes: 43 additions & 0 deletions src/quadrat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Training data for sequence-to-function models, from alignments.

A sequence-to-function model consumes pairs of a one-hot DNA sequence
over an input window and a measured signal profile over a shorter,
concentric output window. Everything between a set of alignments and a
batch of such pairs is independent of the architecture downstream, and
that is what this package implements.

The vocabulary used throughout:

``locus``
A genomic coordinate, typically a peak summit or a sampled
background position.

``window``
A fixed-length interval centred on a locus, in two nested
lengths, i.e., an input window supplying sequence and a shorter
output window supplying signal.

``channel``
One measured track.

``cut-site table``
The intermediate representation: a per-contig, position-sorted
Parquet table of ``(position, value)`` pairs for one channel,
written once by the pileup and read many times by the loader.

``draw``
One ``(locus, jitter offset, strand)`` triple the sampler emits, so
a locus yields many draws under jitter and reverse-complement
augmentation.

Nothing the ``pileup`` extra installs is imported at package import
time, so a caller who already has cut-site tables pays for none of it.
The engine behind that extra reads alignments through polars-bio and
transpiles its aggregate with giql, and loads what it needs when a
pileup runs.
"""

__all__ = ["__version__"]

# public
__version__ = "0.1.0.dev0"
Loading
Loading