Metadata-Version: 2.4
Name: pedigree-graph
Version: 0.11.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Dist: numpy>=2.2,<3
Requires-Dist: scipy>=1.14,<2
Requires-Dist: numba>=0.60,<1 ; extra == 'test'
Requires-Dist: pytest>=9,<10 ; extra == 'test'
Requires-Dist: pytest-cov>=7,<8 ; extra == 'test'
Requires-Dist: pytest-xdist>=3.8,<4 ; extra == 'test'
Requires-Dist: ruff>=0.16,<0.17 ; extra == 'test'
Requires-Dist: hypothesis>=6,<7 ; extra == 'test'
Requires-Dist: pandas>=3.0,<4 ; extra == 'test'
Requires-Dist: polars>=1.43.2,<2 ; extra == 'test'
Requires-Dist: pyarrow>=17,<26 ; extra == 'test'
Requires-Dist: ty==0.0.70 ; extra == 'typecheck'
Provides-Extra: test
Provides-Extra: typecheck
License-File: LICENSE
Summary: Pedigree relationship extraction and kinship on a Rust row-streaming core
Author: Ryan Waples
License-Expression: MIT
Requires-Python: >=3.13
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/rwaples/pedigree-graph/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/rwaples/pedigree-graph
Project-URL: Repository, https://github.com/rwaples/pedigree-graph

# pedigree-graph

Pedigree relationship extraction and kinship on a Rust core, with Python and
R bindings.

A row-streaming engine classifies every relationship pair up to the fifth
degree one individual at a time, so its working memory is linear in the
pedigree size; pair lists, counts, and per-person burden all come from it.
Each relationship type is parameterised by `(up, down, n_ancestors)`:

- `up`: meioses from the first member (the junior role: offspring, descendant, niece_nephew, junior_cousin) up to the common ancestor(s); collateral categories are stored `up ≥ down`
- `down`: meioses from common ancestor(s) down to individual B
- `n_ancestors`: 1 (half/lineal) or 2 (full, i.e. mated pair)
- `kinship = n_ancestors × (1/2)^(up + down + 1)`

## Install

```bash
pip install pedigree-graph
```

Binary wheels cover manylinux x86-64 and AArch64, macOS x86-64 and Apple
Silicon, and Windows x86-64 (one `abi3` wheel per platform for CPython >= 3.13).
Elsewhere pip builds the sdist, which needs a Rust toolchain (`rustc >= 1.85`).

For development the locked pixi environment carries Python, Rust, and maturin
and installs the package editable (the extension module is rebuilt with
`pixi run maturin develop --release` after a Rust change):

```bash
git clone https://github.com/rwaples/pedigree-graph.git
cd pedigree-graph
pixi install --locked
pixi run test       # inner loop: everything but the `slow` tests, about 45 s
pixi run test-all   # before a commit: the full suite, about 100 s
pixi run test-rust  # the Rust core
```

Six of the ten `slow` tests are the `random_30k` integration gates, which run
the kernels over the whole 30,300-row pedigree.  One of them, the frozen
v0.7.1 parity gate, takes most of the slow tier's time (about 80 s of 88 s
measured on 2026-09-24); the rest take 2 to 14 s each.  The other four are the
N=2000 effective-size scaling tests in `tests/test_effective_size_scaling.py`.

Requires Python ≥ 3.13.  Runtime deps: `numpy` and `scipy`.  Construction,
relationships, topology, pairwise kinship, the kinship matrices, the
generation kinship summary, inbreeding, the lineage counts and the Ne
prerequisites are compiled Rust (`pedigree_graph._native`, sources under
`crates/`).  `numba` is only a `test` extra, for the retired kernels the test
suite keeps as oracles.
Pandas is optional and only needed if you pass DataFrames to the
constructors.

### R

The R package `pedigreegraph` (under `r/`) runs the same Rust core. Each
GitHub release from v0.10.0 carries its source tarball, which bundles every
Rust dependency and builds offline given a Rust toolchain (`rustc >= 1.85`):

```r
install.packages(
  "https://github.com/rwaples/pedigree-graph/releases/download/v0.10.0/pedigreegraph_0.10.0.tar.gz",
  repos = NULL, type = "source"
)
library(pedigreegraph)
pg <- pedigree_graph(data.frame(
  id = 1:6, mother = c(NA, NA, 1, 1, NA, 3), father = c(NA, NA, 2, 2, NA, 5)
))
pairs <- relationship_pairs(pg, max_degree = 3)    # one row per pair
pairs$kinship <- pair_kinship(pg, pairs$first, pairs$second)
K <- kinship_matrix(pg)                            # Matrix::dsCMatrix
F <- inbreeding(pg)
```

It covers construction, relationship pairs, pairwise kinship, the complete
kinship matrix and inbreeding; views, counts, lineage and effective size are
Python-only for now. For development, the `r` pixi environment carries R:
`pixi run -e r r-test` installs `r/` in place and runs its tests,
`pixi run -e r r-doc` regenerates `r/man/`, and
`pixi run -e r tools/r_build_tarball.sh` builds the source tarball.

## Usage

```python
import numpy as np
from pedigree_graph import RELATIONSHIPS, PedigreeGraph

# Construct from arrays (no pandas needed)
pg = PedigreeGraph.from_arrays(
    ids=np.array([0, 1, 2, 3, 4]),
    mother_ids=np.array([-1, -1, 0, 0, 0]),
    father_ids=np.array([-1, -1, 1, 1, 1]),
)

# Or from a table: a dict of columns (pandas-free), or any pandas/polars frame
pg = PedigreeGraph.from_frame(
    {
        "id": np.array([0, 1, 2, 3]),
        "mother": np.array([-1, -1, 0, 0]),
        "father": np.array([-1, -1, 1, 1]),
        "twin": np.array([-1, -1, -1, -1]),
        "sex": np.array([0, 1, 0, 1], dtype=np.int8),
        "generation": np.array([0, 0, 1, 1], dtype=np.int32),
    }
)
# pg = PedigreeGraph.from_frame(df)

# Pairs by relationship category, up to a given degree; each block holds
# read-only int32 first_rows / second_rows in the category's role orientation.
# execution="memory" returns the same blocks at the lowest peak memory
# (the result plus engine state) for about twice the wall time.
pairs = pg.relationship_pairs(max_degree=3)
first, second = pairs["FS"]  # full sibs
print(len(pairs["1C"]))  # 1st cousins (degree 3)
print(RELATIONSHIPS["FS"].nominal_kinship)  # 0.25

# Pedigree-expected kinship for those pairs, or for any two row arrays
kin = pg.pair_kinship(pairs)  # {code: float32 array}
kin_fs = pg.pair_kinship(first, second)

# Exact counts in O(N) memory (no pair lists), and the three kinship-matrix families
counts = pg.relationship_counts(max_degree=3)
K = pg.kinship_matrix()  # complete, CSC float32

# One O(N)-memory pass for full-graph category counts and per-person
# degree-1..5 burden, without materializing pair lists.
burden = pg.relationship_burden()
print(burden.category_counts["FS"])
print(burden.per_person[2, 0])  # degree-1 relatives of graph row 2

# Scalar counts for MZ, MO, FO, FS, MHS and PHS only, also exact
close = pg.close_relative_counts()  # no degree/category selector
assert close["FS"] == counts["FS"]
assert close["GP"] is None  # not computed, not zero
```

Absent optional columns read as absent: there is no sex default and no
generation fallback.  Effective-size estimators live in
`pedigree_graph.effective_size`; the `FrameLike` protocol in
`pedigree_graph.typing`.  Migrating from 0.7.1: see the old-to-new table in
`CHANGELOG.md`.

`close_relative_counts()` is full-graph only. Its `requested` and `exact`
sets contain the six codes above; all other registry keys map to `None`.
It counts parent edges and sibling groups without pair lists or adjacency
powers. For grandparents, avuncular pairs, cousins, or a pedigree view, use
`relationship_counts()` instead. The former `estimate_relationship_counts`
method and the result fields `approximate` and `clamped` have been removed.

## Relationship registry

Codes follow the convention `up_down_n_anc`:

| Code   | Label                         | up | down | n_anc | Kinship | Degree |
|--------|-------------------------------|----|------|-------|---------|--------|
| `MZ`   | MZ twin                       | 0  | 0    | 0     | 0.5     | 0      |
| `MO`   | Mother–offspring              | 1  | 0    | 1     | 0.25    | 1      |
| `FO`   | Father–offspring              | 1  | 0    | 1     | 0.25    | 1      |
| `FS`   | Full sib                      | 1  | 1    | 2     | 0.25    | 1      |
| `MHS`  | Maternal half sib             | 1  | 1    | 1     | 0.125   | 2      |
| `PHS`  | Paternal half sib             | 1  | 1    | 1     | 0.125   | 2      |
| `GP`   | Grandparent                   | 2  | 0    | 1     | 0.125   | 2      |
| `Av`   | Avuncular                     | 2  | 1    | 2     | 0.125   | 2      |
| `1C`   | 1st cousin                    | 2  | 2    | 2     | 0.0625  | 3      |
| ...    | (full registry up to 2nd cousin / kinship 1/64) | | | | | |

See `RELATIONSHIPS` for the complete list; each `RelationshipCategory`
carries `code`, `label`, `degree`, `nominal_kinship`, `up`, `down`,
`ancestor_count`, and the two positional roles.

## Architecture

For contributors: [`docs/architecture.md`](docs/architecture.md) maps the
module layout and the hidden contracts (coordinate space, exact count
coverage, sparse-ID handling, default sex), each with its source
of truth and regression test.
The relationship/coordinate vocabulary is in [`CONTEXT.md`](CONTEXT.md);
design decisions are in [`docs/adr/`](docs/adr/).

## License

MIT — see [LICENSE](LICENSE).

