
Genetic models: writing, generating and measuring QTL effects
Source:vignettes/genetic-models.Rmd
genetic-models.RmdA trait’s genetics in tidybreed is a set of
genome-effect terms: additive, dominance and
additive-by-additive (A x A) coefficients on QTL, stored in the
genome_effects tables and evaluated into
ind_tgv by add_tgv(). There are four ways in,
one per job:
| Job | Function |
|---|---|
| You know the coefficients |
define_genome_effect_terms() with
ad_terms(), aa_terms(),
genotype_terms()
|
Sample additive effects that hit a target G
exactly |
define_additive_effects() |
Sample additive + dominance + A x A effects that hit
G_A, G_D, G_AA exactly |
define_genome_effects() |
| Measure what a population actually has | extract_genetic_variance() |
The two generators sample and calibrate: their
effects are written under the reserved owner "generated",
which means “scaled to the stored target”. Known coefficients always go
through the writer, under your own owner.
A small population
Everything below runs on a 60-locus genome held in memory.
library(tidybreed)
library(dplyr)
set.seed(1)
pop <- open_pop(pop_name = "models", db_name = ":memory:") |>
define_genome(n_loci = 60, n_chr = 2, chr_len_Mb = 100) |>
define_founder_haplotypes(n_haplotypes = 400)
pop <- pop |> get_table("founder_haplotypes") |>
add_founders(n_males = 100, n_females = 100, line_name = "A")
for (t in c("ADG", "BF", "WW", "FI", "LS")) pop <- define_trait(pop, t)1. Known coefficients: the writer
With functional coding, a is half the homozygote
difference, d the heterozygote’s deviation and
e the A x A coefficient on the centred dosages.
p is required: it is the basis of the reported mean. The
builders only build rows; define_genome_effect_terms()
writes them.
pop <- pop |>
define_genome_effect_terms(
trait_name = "ADG",
terms = rbind(
ad_terms(locus_name = c("Locus_10", "Locus_44"),
a = c(0.30, -0.12), d = c(0.10, 0.05), p = c(0.35, 0.60),
coding = "functional"),
aa_terms(locus_name_1 = "Locus_10", locus_name_2 = "Locus_44",
e = 0.08, p_1 = 0.35, p_2 = 0.60, coding = "functional")))
#> Implied genetic mean (functional coding), reported only — written to no table: Locus_10 mu = -0.0445; Locus_44 mu = 0.0000. Running total over these single-locus main effects: -0.0445.
#> Implied genetic mean (functional coding), reported only — written to no table: (Locus_10, Locus_44) mu = -0.0048. Running total over these pairs: -0.0048.
#> Wrote 5 genome-effect terms (6 member rows) for trait 'ADG' under owner 'custom' [append].
pop <- pop |> get_table("ind_meta") |> add_tgv("ADG")
#> Computed TGV for 200 individuals on trait 'ADG' (additive, indicator, interaction).
get_table(pop, "ind_tgv") |> collect() |> arrange(id_tgv) |> head(6)
#> # A tibble: 6 × 5
#> id_tgv id_ind trait_name component_name tgv_value
#> <int> <chr> <chr> <chr> <dbl>
#> 1 1 A_1 ADG additive 0.42
#> 2 2 A_1 ADG indicator 0
#> 3 3 A_1 ADG interaction -0.08
#> 4 4 A_10 ADG additive 0.12
#> 5 5 A_10 ADG indicator 0.1
#> 6 6 A_10 ADG interaction 0Each individual gets one value per component; the
ind_tgv_total view adds them. Functional coding stores the
heterozygote deviation as an indicator term on the
heterozygous genotype, so the components are the functional ones
(additive, indicator,
interaction), not breeding values: the breeding value of
this model also carries part of d and e.
Nothing here is calibrated to a variance: a hand-written model has the
variance its coefficients give.
2. Additive effects to a target:
define_additive_effects()
Pass a target G and the sampled effects are rescaled so
the additive covariance is G exactly under
the anchor: "genic" (the HWE expectation at the base allele
frequencies, default) or "realised" (the base individuals’
own genotypes). The target is stored in trait_var_comp with
the effects, in one transaction.
G <- matrix(c(1.0, 0.4,
0.4, 2.0), 2, dimnames = list(c("BF", "WW"), c("BF", "WW")))
pop <- get_table(pop, "genome_meta") |>
filter(locus_id <= 40) |>
define_additive_effects(c("BF", "WW"), G = G, seed = 2)
#> The founder pool (pool expectation under random pairing) sees relative spectrum [0.851, 1.01] of the target (sampling LD of 400 haplotypes).
#> Set correlated additive effects for traits: BF, WW (method: shared; base: founder_haplotypes; scope: all lines, both parents' copies): exact under the genic anchor; delivered [BF,BF = 1; BF,WW = 0.4; WW,WW = 2].The message reports what other populations would see (here the
founder pool, with the sampling LD of 400 haplotypes).
line_name = writes a line-specific variant calibrated to
that line’s own target, for crossbreeding designs.
3. Additive, dominance and epistasis:
define_genome_effects()
The same idea with three targets. Dominance degrees are drawn around
dominance_degree_mean (0.19 by default), A x A effects sit
on random pairs of QTL (or the pairs you supply), and all
three blocks are calibrated together, highest order first.
pop <- get_table(pop, "genome_meta") |>
define_genome_effects("FI", G_A = 1, G_D = 0.3, G_AA = 0.2, seed = 3)
#> Drew 30 random A x A pairs: every QTL paired once (floor(60 / 2)), as AlphaSimR does. Pass n_pairs for fewer pairs, or pairs for a chosen design (a locus may then appear in several pairs).
#> Set genome effects for 60 QTL and 30 pairs on trait FI (base: founder_haplotypes): exact under the genic anchor; delivered additive variance 1; dominance variance 0.3; additive-by-additive variance 0.2.
#> Additive floor for this sampled architecture (the least additive variance its dominance and A x A effects allow; another draw gives another floor): FI = 0.3706. G_A - floor has smallest eigenvalue 0.6294 on the correlation scale.
#> Inbreeding depression (drop in the mean per unit of F, positive = depression; sum 2pq d under the genic anchor's frequencies): implied FI = 3.114.
#> Functional effects (sampling: dominance_degree_mean = 0.19, dominance_degree_sd = 0.097): FI: 60 QTL with a != 0, 60 with d != 0, 30 pairs with e != 0.
#> The founder pool (pool expectation under random pairing; additive block only, dominance and additive-by-additive not compared) sees relative spectrum [0.992, 0.992] of the target (sampling LD of 400 haplotypes).The messages report the additive floor of this draw (below), the
inbreeding depression the dominance effects imply
(inbreeding_depression = sets it instead), and the founder
pool’s view. A re-run replaces the trait’s "generated"
terms and never touches terms you wrote yourself.
The additive floor. Dominance and epistasis induce
additive effects of their own (an allele substitution changes the
dominance and pair values too), so the additive variance cannot be made
arbitrarily small. A G_A below what the drawn architecture
can cancel is refused, with the floor named:
pop <- get_table(pop, "genome_meta") |>
filter(locus_id <= 10) |>
define_genome_effects("LS", G_A = 0.001, G_D = 1, seed = 4)
#> Error:
#> ! `G_A` is below the additive floor for this sampled architecture under the "genic" anchor. The dominance and additive-by-additive effects already induce additive variance that the drawn additive architecture cannot cancel: at least LS = 0.451916 (the floor's diagonal; G_A - floor has smallest eigenvalue -450.9 on the correlation scale). Raise `G_A` or lower `G_D` / `G_AA`. The floor belongs to this draw: another seed gives another floor. Nothing was written.The floor belongs to the sampled architecture: another seed, or more loci, gives another.
4. Measuring: extract_genetic_variance()
The extractor decomposes the stored model on any group of
individuals, under the same two anchors, and writes nothing. Its rows
use trait_var_comp’s column names, so a target comparison
is a join.
measured <- get_table(pop, "ind_meta") |>
extract_genetic_variance(c("BF", "WW"), anchor = "realised")
#> Realised genetic covariances of the 200 selected individuals (frequencies and regressions are the cohort's own).
measured
#> # A tibble: 12 × 7
#> effect_name trait_name_1 trait_name_2 cov_value n_ind decomposition anchor
#> <chr> <chr> <chr> <dbl> <int> <chr> <chr>
#> 1 additive BF BF 0.991 200 full reali…
#> 2 additive BF WW 0.274 200 full reali…
#> 3 additive WW BF 0.274 200 full reali…
#> 4 additive WW WW 1.65 200 full reali…
#> 5 between_compo… BF BF 0 200 full reali…
#> 6 between_compo… BF WW 0 200 full reali…
#> 7 between_compo… WW BF 0 200 full reali…
#> 8 between_compo… WW WW 0 200 full reali…
#> 9 total BF BF 0.991 200 full reali…
#> 10 total BF WW 0.274 200 full reali…
#> 11 total WW BF 0.274 200 full reali…
#> 12 total WW WW 1.65 200 full reali…
targets <- get_table(pop, "trait_var_comp") |> collect()
inner_join(measured, targets,
by = c("effect_name", "trait_name_1", "trait_name_2"),
suffix = c("_measured", "_target")) |>
select(effect_name, trait_name_1, trait_name_2,
cov_value_measured, cov_value_target)
#> # A tibble: 4 × 5
#> effect_name trait_name_1 trait_name_2 cov_value_measured cov_value_target
#> <chr> <chr> <chr> <dbl> <dbl>
#> 1 additive BF BF 0.991 1
#> 2 additive BF WW 0.274 0.4
#> 3 additive WW BF 0.274 0.4
#> 4 additive WW WW 1.65 2The generation targets were calibrated under the genic anchor at the
founder pool’s frequencies; these 200 individuals are a finite sample of
it, so the realised values differ a little.
anti_join(targets, measured, ...) lists targets with no
measured block (a trait without terms, say).
What “exact” promises
- A generator’s result is exact for a feasible target under the named anchor and the selected QTL: other populations, later generations and other anchors see what their frequencies and LD give. Every call reports one such comparison.
- A line-scoped call calibrates its own variant only.
-
method = "union"(each trait its own QTL set) delivers the variances exactly but the covariances only approximately, with a warning that gives them;method = "shared"is exact. - One call writes one
parent_originscope. - The extractor measures the population you select. Compare like with like: the generation anchor, base and scope.