Skip to main content

Declarative AADR panel subsetting from YAML selectors; the missing first-class tool for cohort definitions in ancient-DNA / population-genetics workflows.

Project description

aadr-subset

Declarative AADR panel subsetting from YAML selectors. Replaces ad-hoc awk pipelines and one-off scripts with version-stable, PR-reviewable cohort definitions. Built on top of aadr-resolve for cross-AADR-version sample-ID mapping.

# britain_iron_age.yaml
populations: ["England_IA*"]   # matches England_IA + v62 .AG/.SG/.DG variants
date: {min_calbp: 1900, max_calbp: 2400}
min_coverage: 0.3              # 1240k-target coverage (unitless ratio)
exclude:
  individual_ids: [I12345]     # known contaminated sample
$ aadr-subset select britain_iron_age.yaml v66.HO.aadr.PUB.anno -o cohort.ids
Selector: britain_iron_age.yaml (sha256:1a2b3c4...d5e6f7g)
.anno:    v66.HO.aadr.PUB.anno (v66.0, class E)

Matched 45 samples across 1 population.

Per-population: England_IA=45

Wrote cohort.ids (45 lines)
Done in 0.18s (parse 0.16s, eval 0.02s, write 0.00s).

$ plink2 --pfile aadr_v66 --keep cohort.ids --make-pgen --out britain_iron_age

Why it exists

Ancient-DNA workflows live and die on cohort definitions — which samples go into this analysis. Today that's typically a hand-curated set of Group_ID literals in someone's shell script, prone to: silent breakage when AADR releases a new version with renamed labels; no version pinning in commit history; no way to share the exact cohort between collaborators short of swapping .ind files.

aadr-subset makes the cohort itself a first-class artifact:

  • Selector YAMLs are version-stable. They cite AADR releases via tested_against: metadata; the selector_signature (RFC 8785 JCS SHA-256 over the canonical form) gives you a hash that survives YAML formatting churn.
  • Reviewable in PRs. The grammar is flat (top-level AND with one-level any: OR and one-level exclude: NOT). What you see is what runs.
  • Cross-version via aadr-resolve. resolve_to_version: lifts Individual_IDs from an older release to the newer one through the GID-stable bridge + MID-rename map.
  • Six subcommands cover the full lifecycle: validate, select, inspect, report, diff, template.

Install

pip install aadr-subset

Python 3.11+. The only external dependency is aadr-resolve, pulled in automatically.

For development setup see CONTRIBUTING.md.

The six subcommands

validate SELECTOR.yaml

JSON-schema + semantic-constraint check on a selector. No .anno required. Useful as a CI gate.

$ aadr-subset validate britain_iron_age.yaml
# exit 0 on valid; exit 4 on schema or semantic violation
# Errors carry precise file:line:col + JSON pointer:
$ aadr-subset validate broken.yaml
broken.yaml:7:5: at /populations/2: 42 is not of type 'string'
broken.yaml:12:3: at /any/0/min_coverage: -0.5 is less than the minimum of 0

select SELECTOR.yaml ANNO.anno [ANNO2.anno …] [-o PATH] [--format ids|tsv|json]

The main case: materialize a selector against a target .anno and write matched sample IDs / TSV / JSON.

aadr-subset select britain_iron_age.yaml v66.HO.aadr.PUB.anno -o cohort.ids
aadr-subset select britain_iron_age.yaml v66.HO.aadr.PUB.anno --format tsv -o cohort.tsv
aadr-subset select britain_iron_age.yaml v66.HO.aadr.PUB.anno --format json -o cohort.json

Stratified sampling caps can be specified inline on the CLI (or in the selector YAML — selector wins per-field):

# cap to 1 library per individual (dedup .AG/.SG/.DG triplicates),
# then at most 50 samples per population
aadr-subset select europe_neolithic.yaml v66.HO.aadr.PUB.anno \
    --max-per-individual 1 --max-per-population 50 -o cohort.ids

Multi-anno select (v0.4+): pass more than one .anno to merge cohorts across releases in a single command. The selector is evaluated against each .anno independently; results are union-deduplicated on genetic_id (newer-version rows win on collision):

aadr-subset select britain_iron_age.yaml v44.3_HO.anno v66.HO.aadr.PUB.anno \
    -o cohort.ids

TSV output gains a source_version column identifying which .anno each row came from. JSON output gains anno_versions, anno_files, and per_anno_n_matched keys alongside the single-anno fields for backwards compatibility. resolve_to_version: selectors are incompatible with multi-anno mode (hard error — use single-anno + --source-anno for cross-version IID lifting instead).

Cross-version flow (selector defined against an older release than the materialized one):

# britain_v62_lift.yaml
individual_ids: [I12345, I12346]
source_version: v62.0
resolve_to_version: v66.0
aadr-subset select britain_v62_lift.yaml v66.HO.aadr.PUB.anno \
    --source-anno v62.0_HO_public.anno \
    -o lifted.ids

The positional .anno is the target (where the lifted cohort materializes); --source-anno is the source (where the selector's Individual_IDs are originally defined). aadr-resolve bridges the two.

v62.0 inputs (class D — no native coverage column) need a derived proxy for min_coverage: filters:

aadr-subset select britain_iron_age.yaml v62.0_HO_public.anno \
    --coverage-derive snps_hit_1240k -o cohort.ids

inspect SELECTOR.yaml ANNO.anno

Dry-run: shows what a selector matches without writing any file. Always exits 0 — meant for debugging selector logic.

$ aadr-subset inspect britain_iron_age.yaml v66.HO.aadr.PUB.anno
Selector: britain_iron_age.yaml
.anno:    v66.HO.aadr.PUB.anno (v66.0, class E, 27,755 samples)

Matched: 45 samples across 1 population

Per-population breakdown:
  England_IA  45

Branch contributions:
  top_level  45

Date range of matched: 1934 - 2398 calBP (median 2103)
Coverage range:        0.34 - 4.81x (median 1.28)

Selector signature: sha256:1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e

When sampling caps are active a "Downsampled" section appears (v0.3+). Per-population entries are listed individually; per-individual drops are aggregated (they can run into the thousands; the JSON output preserves per-IID detail for callers that need it):

$ aadr-subset inspect europe_neolithic.yaml v66.HO.aadr.PUB.anno \
      --max-per-individual 1 --max-per-population 50
...
Matched: 312 samples across 8 populations

Downsampled:
  Anatolia_N  12 samples dropped
  Iran_N       8 samples dropped
  per-individual aggregate: 177 samples dropped across 177 individual(s)

report SELECTOR.yaml ANNO.anno [-o PATH] [--format tsv|json]

Per-population aggregates: how many samples each Group_ID contributed, with date range and coverage stats.

$ aadr-subset report britain_iron_age.yaml v66.HO.aadr.PUB.anno
group_id    n_matched   n_in_anno   pct_matched   date_min_calbp   date_max_calbp   coverage_median
England_IA  45          51          88.2          1934             2398             1.28

--include-empty-groups adds rows for .anno groups that matched zero samples (useful for population-survey workflows).

diff SELECTOR_A.yaml SELECTOR_B.yaml ANNO.anno [-o PATH] [--format human|json]

Set-difference of two selectors against the same .anno: which samples does A match that B doesn't, and vice versa, plus a per-population delta. Always exits 0 — diagnostic, not a gate. Useful for PR review of selector changes.

$ aadr-subset diff old.yaml new.yaml v66.HO.aadr.PUB.anno
Selector A: old.yaml (sha256:1a2b3c4...d5e6f7g)
Selector B: new.yaml (sha256:9z8y7x6...w5v4u3t)
.anno:      v66.HO.aadr.PUB.anno (v66.0, class E)

A only: 5 samples
B only: 12 samples
Both:   38 samples

Per-population delta:
  group_id             A   B  delta
  England_IA          43  40     -3
  England_IA-o         0  10    +10
  England_BellBeaker   0   2     +2

A only sample preview: ['I12345', 'I12346', 'I12347', 'I12348', 'I12349']
B only sample preview: ['I20001', 'I20002', ...] (+2 more)

--format json -o diff.json writes a structured object with a_only[], b_only[], both[], per_population_delta[] arrays plus both signatures — suitable for pipeline integration / dashboards.

template [NAME] [-o PATH]

Ships starter selectors for common cohorts. No-arg form lists shipped templates; arg form emits the verbatim YAML (comments + metadata block preserved) to stdout or --out PATH.

$ aadr-subset template
bronze_age_europe
iron_age_britain
modern_european
neolithic_anatolia
viking_period_scandinavian
wsh_steppe_pool

$ aadr-subset template iron_age_britain -o britain.yaml
# britain.yaml now contains a working starting point — edit + extend.

All shipped templates are verified against AADR v62.0 and v66.0 — each template's tested_against: metadata reflects the releases it resolves to non-zero matches against.

Exit codes

Code Meaning
0 Success
1 Soft validation failure (e.g. zero-match without --allow-empty, --strict-resolve missing IIDs)
2 I/O failure (file not found, .anno schema unrecognized, etc.)
3 Invariant violation (internal consistency check failed — please file an issue)
4 Usage error (schema violation, flag misuse, unknown template)
70 Internal error (uncaught exception escape hatch — please file an issue)

Selector grammar (overview)

Flat — one level of nesting maximum. Top-level keys AND-combine.

# Top-level AND
populations: [Western_HG, "England_*"]   # group_id literals + fnmatch globs (v0.2+)
individual_ids: [Loschbour, KO1]         # match against individual_id
individual_ids_source: ids.txt           # newline-delimited file
modern_only: true                        # shorthand: date_calbp <= 70
min_coverage: 0.3
coverage_column: snps_hit_1240k          # override; selector-side wins over --coverage-derive
date:
  min_calbp: 1900
  max_calbp: 2400
source_version: v62.0                    # cross-version lift
resolve_to_version: v66.0

# One-level OR (matches any branch)
any:
  - populations: [Western_HG]
    min_coverage: 1.0
  - populations: [Eastern_HG]
    min_coverage: 0.5

# One-level NOT-of-OR (drops matches)
exclude:
  group_ids: [English.SG, "*_o.SG"]      # literals + globs
  individual_ids: [I12345]

# Stratified sampling caps (v0.3+; applied after exclude, before dedup)
sampling:
  max_per_population: 50                 # cap per group_id (integer ≥ 1)
  max_per_individual: 1                  # cap per individual_id (1 = pick best library)
  policy: top_coverage                   # default; v0.3 ships only this

Group_ID globs (v0.2+): any string containing *, ?, or [abc] is treated as an fnmatch pattern against the target .anno's Group_IDs. Patterns work in populations:, exclude.group_ids:, and any-branch populations:. The selector signature hashes the pattern, not the resolved set — so the same selector against v62 vs v66 produces the same signature even when the pattern resolves to different concrete labels. A pattern that matches zero Group_IDs surfaces as a warning (CLI: stderr; library API: logging.getLogger("aadr_subset")) — likely a typo.

Stratified sampling (v0.3+): sampling.max_per_population / max_per_individual cap the cohort within each Group_ID / Individual_ID. Per-individual fires first, then per-population — max_per_individual: 1 is the canonical "one library per individual" dedup; combined with a per-population cap it picks the cap-many distinct individuals with the highest coverage. --max-per-population N / --max-per-individual N CLI flags also work; selector wins per-field. Both feed the signature (intent-not-expansion — same caps against v62 vs v66 = same hash). On class-D inputs (v62.0, no native coverage column), sampling requires --coverage-derive snps_hit_1240k for priority — without it, sampling errors out (the engine refuses to "prioritize" against an undefined coverage column).

For the full grammar reference see the JSON Schema; for grammar semantics and edge cases (NaN handling, branch independence, signature canonicalization) see the inline schema description: fields and the CHANGELOG.

Library API (v0.4+)

aadr-subset exposes a Python API for programmatic use — no subprocess required:

from aadr_subset import select, SubsetResult

result: SubsetResult = select("britain_iron_age.yaml", "v66.HO.aadr.PUB.anno")
print(result.genetic_ids)      # list[str] of matched sample IDs
print(result.n_matched)        # int
print(result.anno_version)     # e.g. "v66.0"
print(result.selector_signature)  # "sha256:..."

Full signature:

from pathlib import Path
from aadr_subset import select, load_selector
import aadr_resolve

result = select(
    selector,             # str | Path | Selector — YAML path or pre-loaded object
    anno,                 # str | Path | AnnoFrame — .anno path or pre-loaded object
    *,
    allow_empty=True,     # True by default (differs from CLI default of False)
    allow_empty_source=False,
    include_matched_criteria=False,
    source_anno=None,     # str | Path | AnnoFrame — cross-version source
    mid_bridge=None,      # str | Path — MID-rename bridge for multi-anno dedup
    strict_resolve=False,
    coverage_column=None, # str — override coverage column
    coverage_derive=None, # str — derive proxy for class-D inputs
    max_per_population=None,   # int
    max_per_individual=None,   # int
    schema_override=None,      # str — force schema class detection
    quiet=False,
)

Key differences from the CLI:

  • allow_empty=True by default — the API doesn't abort on zero matches; callers check result.n_matched themselves.
  • Warnings via logging — the "aadr_subset" logger emits warnings (zero-match globs, v62 coverage proxy) instead of writing to stderr. Configure with logging.getLogger("aadr_subset").
  • Pre-loaded objects accepted — pass a Selector or AnnoFrame directly to skip repeated file I/O in loops.

Public symbols exported from aadr_subset: select, load_selector, SubsetResult, Selector, SelectorMetadata, SamplingSpec, AadrSubsetError, IOFailure, InvariantViolation, SoftValidationFailure, UsageError, ValidationError.

Composing with plink2

# Materialize a cohort
aadr-subset select britain_iron_age.yaml v66.HO.aadr.PUB.anno -o cohort.ids

# Use it as a plink2 keep set
plink2 --pfile aadr_v66 \
       --keep cohort.ids \
       --make-pgen --out britain_iron_age_subset

select --format json produces a structured artifact suitable for pipeline metadata logging (records the selector signature, AADR version, schema class, and effective coverage column).

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aadr_subset-0.4.1.tar.gz (73.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aadr_subset-0.4.1-py3-none-any.whl (80.1 kB view details)

Uploaded Python 3

File details

Details for the file aadr_subset-0.4.1.tar.gz.

File metadata

  • Download URL: aadr_subset-0.4.1.tar.gz
  • Upload date:
  • Size: 73.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aadr_subset-0.4.1.tar.gz
Algorithm Hash digest
SHA256 dacac47bf59066025fa5c48ab96e12d20bf4cf7e4222ba143b28e5dd44977f87
MD5 9bf33349161b3b858915f68a40dfce42
BLAKE2b-256 9e504d0106da2a8b485057d7eea1de176061e54b1a8210986596da1ce90fcefb

See more details on using hashes here.

Provenance

The following attestation bundles were made for aadr_subset-0.4.1.tar.gz:

Publisher: release.yml on carstenerickson/aadr-subset

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aadr_subset-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: aadr_subset-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 80.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aadr_subset-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 74ef3bcbc8432f3a1bf1cfd9adb2285914a93e04d219d01fd0c2b4b15774f7db
MD5 3603e0bdf64946faed3d9e4cedccd6e7
BLAKE2b-256 6c4450bd3d0c5426e7aa7741a24608b3b4e9abd4ea9b0dbbd46557422fccbca1

See more details on using hashes here.

Provenance

The following attestation bundles were made for aadr_subset-0.4.1-py3-none-any.whl:

Publisher: release.yml on carstenerickson/aadr-subset

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page