User guide¶
This guide uses Python. The R interface has the same functions and arguments; see Using cointf from R for what differs.
Installation¶
From a clone of the repository:
pip install . # numpy and scipy are installed as dependencies
pip install -e ".[tests]" # for development: editable install plus the test tools
pytest # optional: run the test suite
Data¶
Crossover data are a list with one entry per meiosis. Each entry holds that meiosis’s crossover positions in Morgans, and may be empty.
data = [[0.31, 1.12], [0.70], []] # three meioses; the third has no crossovers
L = 1.5 # chromosome length in Morgans (or one value per meiosis)
Positions must be finite, lie in \([0, L]\) and be distinct within a meiosis. Order does not matter.
Invalid data raise ValueError with the index of the offending meiosis.
Preparing data once: Meioses¶
Meioses validates and sorts the positions, groups meioses by crossover count and precomputes
every distance the likelihood needs. Build it once and reuse it for every parameter value:
from cointf import Meioses, loglik_fast
mz = Meioses(data, L) # L may be a scalar or one value per meiosis
mz # Meioses(M=3, crossovers=3, max_count=2)
loglik_fast(mz, None, nu=4.0, p=0.05)
When passing a Meioses, the length argument must be None, because the object carries its own
lengths.
Single-meiosis likelihood: loglik_fast¶
loglik_fast(data, L, nu, p, *, kernel=None, per_meiosis=False)
nu > 0: interference parameter.nu = 1is no interference.0 <= p <= 1: fraction of crossovers escaping interference.per_meiosis=Truereturns one log-likelihood per meiosis instead of the sum.
total = loglik_fast(data, 1.5, nu=4.0, p=0.05)
each = loglik_fast(data, 1.5, nu=4.0, p=0.05, per_meiosis=True) # shape (3,)
Parameters that vary between meioses¶
nu, p and L each accept a scalar or an array with one value per meiosis. For example, nu
can depend on a covariate:
import numpy as np
age = np.array([25.0, 31.0, 40.0])
nu = np.exp(1.2 + 0.01 * age) # one nu per meiosis
loglik_fast(data, 1.5, nu=nu, p=0.05, per_meiosis=True)
An array whose entries are all equal is treated as the scalar, so a pooled fit costs the same whichever form is passed.
Kernels¶
The likelihood needs \(\log f^*\) at the gaps between crossovers and \(\log S^*\) at the distances to the chromosome ends. These come from a kernel, a function
kernel(u_gap, nu_gap, u_end, nu_end) -> (log_f, log_S)
The default, series_kernel, evaluates the exact gamma series. ChebyshevKernel replaces it
with piecewise Chebyshev fits that are built once per nu and cached:
from cointf import ChebyshevKernel
kernel = ChebyshevKernel() # keep one instance, so its cache is reused
loglik_fast(mz, None, nu=4.0, p=0.05, kernel=kernel)
When to use it:
Shared
nu(pooled fits, profiling overp, grids): 6–10× faster, with log-likelihood differences around 1e-13 per meiosis. Building a fit for a newnutakes 3–9 ms, which is paid once per distinctnu.Many distinct
nuvalues in one call (more thanmax_distinct, default 16): the kernel falls back to the exact series, so there is no speed-up yet.Distances outside
[u_min, u_max](default[1e-6, 5], on the interference scale) always use the exact series. Longer maps are therefore still correct, but gain less.
Any callable with the kernel signature can be passed, for example to experiment with other
approximations. The outputs must have the same shapes as u_gap and u_end.
Phase-unknown data: loglik_quartet¶
For a parent in a family with two offspring and no grandparents, each chromosome yields the pooled crossovers of two meioses, with no record of which meiosis each crossover came from. Pass one entry per quartet:
from cointf import loglik_quartet, simulate_quartets
quartets = simulate_quartets(L=1.5, nu=4.0, p=0.05, n=200, seed=7)
loglik_quartet(quartets, 1.5, nu=4.0, p=0.05)
loglik_quartet(quartets, 1.5, nu=4.0, p=0.05, kernel=kernel) # same kernels work
The interface matches loglik_fast. per_meiosis=True returns one value per quartet (the
name is kept so the two functions are interchangeable), and a Meioses built from quartet data
holds one entry per quartet.
Simulation¶
from cointf import simulate_meiosis, simulate_meioses, simulate_quartets
simulate_meioses(L=1.5, nu=4.0, p=0.05, n=1000, seed=1) # list of sorted arrays
simulate_quartets(L=1.5, nu=4.0, p=0.05, n=1000, seed=1) # two meioses pooled per entry
The simulator draws the first interference crossover from the stationary distribution, so simulated data follow exactly the model the likelihood assumes.
Reference implementations¶
cointf.naive contains slow, direct implementations used to validate the fast code:
loglik_meiosis_naiveandloglik_naive: the \(O(n^2)\) recursion one meiosis at a time, with a fixed 200-term series;loglik_quartet_naive: brute force over all \(3^n\) labelings of a quartet.
Use them for spot checks, not for fitting.
Fitting¶
cointf provides likelihoods, not optimisers. Wrap loglik_fast in any optimiser; working on
\(\log \nu\) and \(\operatorname{logit} p\) keeps the parameters in range. See
the maximum-likelihood example for a complete fit with standard errors.