
Define additive QTL effects for one or more traits
Source:R/define_additive_effects.R
define_additive_effects.RdSelects QTL from a filtered genome_meta table, samples an effect
architecture, calibrates it to the stored additive target, and writes
one order-one additive term per locus and trait through the same engine as
define_genome_effect_terms(), under the reserved effect owner
"generated". Only generators write that owner, so effects written here
and effects a user writes with define_genome_effect_terms() can never be
confused for one another. add_tgv() evaluates both; its additive
component is the breeding value.
Arguments
- tbl
A
tidybreed_tablefromget_table()("genome_meta")(with an optionaldplyr::filter()). The filtered rows determine which loci are QTL.- trait_name
Character scalar or vector. Name(s) of existing traits (from
define_trait()). With two or more, the architecture is drawn jointly fromMVN(0, G),Gis ak x kmatrix across them, andmethodapplies.- distribution
Character. The shape of a single trait's sampled architecture before calibration:
"normal"(default; normal effects) or"gamma"(gamma-distributed sizes with random signs: a few large QTL and many small ones). Calibration then scales the effects to the target either way. Ignored with several traits, which are always drawn fromMVN(0, G).- G
Optional additive-genetic (co)variance target: a
k x kmatrix (named intrait_nameorder, or unnamed), or a single number for one trait. Written totrait_var_comp(with the call'sline_name) in the same transaction as the effects, and never over a stored block.NULLreads the stored target (see Targets).- trait_var_comp_tbl
Optional filtered
get_table(pop, "trait_var_comp"): the stored rows to calibrate to. Use it to pick a block explicitly, e.g. to leave a stored non-additive block out or to calibrate one trait of a stored block alone. Not withG.- anchor
Character.
"genic"(default) or"realised": the reference covariance the calibration is exact for. See Details.- method
Character, multi-trait only.
"shared"(default): every trait uses all the filtered loci as one shared QTL set, and the wholeGis delivered exactly."union": each trait keeps the QTL set of its existing generated terms at this scope (restricted to the filtered loci), so the traits must already have generated effects there; each trait is scaled to its own variance, and the covariances are approximate (a warning gives the delivered values).- base_tbl
Optional
tidybreed_tablefromget_table()(optionally filtered) selecting the allele copies that define base allele frequencies:founder_haplotypes,ind_haplotype, or any table with anid_indcolumn (e.g. the founders,get_table(pop, "ind_meta") |> filter(is.na(id_parent_1))). Must come from the samepopastbl.NULL(default) resolves to the founder pool of the line the effect applies to — see Which population centers the effects. Underanchor = "realised"it is required and names the individuals whoseCov(X)is the anchor.- line_name
Optional character. When set, effects are scoped to allele copies of this genetic line: a copy whose
line_originmatches takes these values, and falls back per copy to the common variant where no line-specific one exists. Also selects the defaultbase_tbland the line's own target block.NULL(default) means the common scope.- parent_origin
Optional
1(sire / parent_1) or2(dam / parent_2) — imprinting, restricting the term to copies inherited from that parent.NULL(default) means both parents' copies. Per trait: a scalar is recycled, a vector must matchtrait_namepositionally, or name its entries by trait; each value must be exactly1or2. One call must use one origin for every trait: it calibrates against one reference covariance, defined for one set of inherited copies (a mixed-scope anchor is not supported yet). Define differently-scoped traits in separate calls. For imprinting that varies locus by locus, write the terms withdefine_genome_effect_terms().- warn_bounds
Numeric length 2,
c(lower, upper)with0 < lower <= upper, orNULLto turn the comparison off. Outside the bounds, an observed or genic-limit comparison warns; a founder-pool comparison adds the realised-anchor hint to its message. Defaultc(0.8, 1.25)(±25%, multiplicative).- seed
Optional integer, applied with
set.seed()immediately before the draw, after every input check and after the anchor's feasibility check (rank(G) <= rank(M)), so those refusals never touch the RNG. A failure that depends on the draw itself – the drawn architecture's rank, or a calibration that fails verification – comes after the seed and has consumed RNG draws.NULL(default) draws from the current stream, so a precedingset.seed()reproduces the call too.
Value
The modified tidybreed_pop (invisibly). The effects are written
to the genome_effects tables under the owner "generated", and a
passed G to trait_var_comp. Run add_tgv() to evaluate them.
Details
When the result is exact. The requested covariance G is delivered
exactly, B' M B = G to machine precision, for one trait or for several
with method = "shared", when the rank is feasible: rank(G) <= rank(M) (the anchor has enough independent
segregating directions at the selected loci) and rank(G) <= rank(B0' M B0) (the drawn architecture does too). Each infeasibility is its
own error. Rank and positive semidefiniteness are judged on the target's
correlation scale, so a trait recorded in small units is never
truncated away. "Exact" is checked, not assumed: the delivered B' M B is
compared with G as stored, entry by entry, to a relative tolerance of
1e-8 on the correlation scale; a calibration that misses it (a
numerically ill-conditioned architecture) is an error before anything is
written. The closing message says "exact" or "approximate" and gives the
delivered covariance under the anchor.
How. Effects are drawn first (one draw per QTL for one trait; joint
MVN(0, G) rows for several), and that draw is only the architecture
B0. It is then right-multiplied by a k x k matrix A so that
B = B0 A satisfies B' M B = G exactly. For one trait this is exactly
the scalar rescale b0 * sqrt(G / sum(w b0^2)). For two or more it also fixes
the genetic correlations, which a per-trait rescale cannot: at 200 QTL
and a target correlation of 0.4 a scalar rescale delivers anything from
about 0.18 to 0.60. The same seed gives the same B0.
The anchor M is the reference-population genotype covariance the
calibration is exact for:
anchor = "genic"(default):M = diag(n_eligible * p * q)at the base allele frequencies, the random-mating (HWE + linkage-equilibrium) limit. Use it for multi-generation studies: under random mating the realised covariance converges to it. Withbase_tblthe weights use that population's frequencies instead, e.g. one reference line.anchor = "realised":M = Cov(X)of the individualsbase_tblselects, linkage disequilibrium included. The single-generation / clonal option.base_tblmust select individuals (a table withid_ind, notfounder_haplotypesorind_haplotype) with complete genotypes at the QTL, and the call must use the common scope (noline_name, noparent_origin). The genotype matrix is collected into memory, so there is a size limit; above it the call errors and suggests"genic".
Under method = "union" each trait keeps its own QTL set and is scaled by
its own variance only, so the variances are exact and the covariances are
approximate – including a zero target covariance, which overlapping QTL
sets do not deliver. A warning gives the delivered covariance and
correlation and names method = "shared" as the exact option. A trait with
a positive target variance and no QTL in the call is an error.
After calibration the delivered covariance is compared with what another
population sees: the pool expectation 2 Cov(H)
when the base is the founder pool, the observed Cov(X) when it selects
individuals, and the genic limit under anchor = "realised". The founder
pool's comparison is always a message: a small pool's departure is its
sampling LD, not a mistake in the call. To get the target exactly in the
founders, add them first and calibrate with anchor = "realised" on them.
The observed and genic-limit comparisons warn when the relative spectrum
leaves warn_bounds. Nothing is stored.
Targets
The target is the population-wide (or line) additive block of
trait_var_comp, the single source of generation targets:
pass
G(ak x kmatrix, or a number for one trait) to write it and calibrate to it, in the same transaction as the effects. If a block is already stored for any of the traits at thatline_namethe call errors, even for an identical matrix, and gives theremove_rows()call;or leave
G = NULLto use the stored rows, optionally chosen withtrait_var_comp_tbl = get_table(pop, "trait_var_comp") |> filter(...). The rows for the call's traits must form one complete symmetric block. A stored block that pairs one of the traits with a trait outside the call is an error (calibrating one trait alone would break the stored covariance), as is a storeddominanceoradditive_by_additiveblock for the traits: this generator calibrates the additive block only and never silently ignores a stored target. Filter them away withtrait_var_comp_tblto say so explicitly.
With line_name = "C" the default reads line C's block when one exists and
otherwise the population-wide one.
A new G must not leave other generated terms describing the old target.
A call replaces only its own scope, so G is refused when generated
additive terms of a line with no target of its own fell back to the target
it would write; the error gives the route (give that line its own G
first). Terms of another parent_origin at the same target scope cannot get
their own target (targets are per line), so the call writes and warns,
naming the scopes to re-run without G.
Generated means calibrated
The generator always samples and calibrates: every term it writes
under the "generated" owner delivers its stored target. That is what lets
define_phenotype()(prevalence = ) trust the stored target. It has no
option to write fixed or unscaled effects. Exact coefficients (GWAS
estimates, a published QTL map, a hand-built test) go through
define_genome_effect_terms() with ad_terms() under a user owner;
add_tgv() evaluates them like any other term, and a liability phenotype
on such a trait then needs thresholds = instead of prevalence =.
Which population centers the effects
Base allele frequencies center the true breeding value (the Falconer
allele - p term) and set the genic weights n_eligible * p q. They come
from base_tbl, a filtered tidybreed_table whose identity says what kind
of thing is selected and whose dplyr::filter() says which. The pipe
subject tbl picks the loci; base_tbl picks the population. Three shapes
are accepted (the same as extract_allele_freq(), which you can call
first to see the p a base gives):
The founder pool –
get_table(pop, "founder_haplotypes"), optionally|> filter(line_name == "Duroc"): the haplotypesdefine_founder_haplotypes()wrote.Allele copies –
get_table(pop, "ind_haplotype") |> filter(line_origin == "Duroc"): exactly those copies, e.g. Duroc-origin copies at any cross depth.Individuals – any table with
id_ind, semi-joined on its distinct ids. The foundersadd_founders()created are theind_metarows with no parents:get_table(pop, "ind_meta") |> filter(is.na(id_parent_1))(addline_name == "Duroc"for one line, or filter on a custom column such asgen == 0L).ind_phenotype,ind_ebvand the like work the same way.
The founder pool and the founders are different bases. add_founders()
samples haplotypes from the pool with replacement, so the founders'
frequencies depart from the pool's by sampling noise. Use the pool for the
expectation, the founder individuals to calibrate to the animals actually
simulated; only individuals can serve as a "realised" anchor.
When base_tbl = NULL the base is the population the effect applies to,
resolved with the same line -> NULL precedence as resolve_genome_map():
a line-specific effect (line_name = "A") centers on line A's own founder
pool, or on the shared (line_name = NULL) pool when no named pool exists;
a population-wide effect (line_name = NULL) centers on the whole founder
table. Only that last case warns when the founder table holds more than one
pool: pooling divergent lines overstates within-line heterozygosity — the
Wahlund effect — so the calibration under-scales the effects and the
realised within-line additive variance falls short of the target. An
explicit base_tbl is an intentional selection and never warns — pass
base_tbl = get_table(pop, "founder_haplotypes") to pool on purpose, which
is how the common fallback variant of a crossbreeding model is defined.
A selected QTL locus with no allele copies in the base is an error, never
silently centered at p = 0.
The centering constant is stored per member as
genome_effect_members.center_value and travels with its genome_value, so
evaluation applies each allele copy's own line's centering — a crossbred
animal's line-A alleles are centered on line A and its line-B alleles on
line B.
Lines and line means
Line-specific effects (line_name = "A") are centred on line A's own base,
so they add no difference between line means. Differences between lines
come from allele-frequency differences at QTL whose effects are shared:
use common effects centred on one reference line, e.g.
base_tbl = get_table(pop, "founder_haplotypes") |> filter(line_name == "Terminal"). The calibration then hits the target
within that line, and the other lines get whatever variance their
frequencies give.
Scope, and what a re-run replaces
line_name and parent_origin compose into the single origin row an
additive member is allowed:
line_name | parent_origin | Stored scope |
NULL | NULL | no origin rows (the common scope) |
"A" | NULL | ('exact', 'A', parent NULL, copy_count = 1) |
NULL | 1 / 2 | ('any', NULL, parent, copy_count = 1) |
"A" | 1 / 2 | ('exact', 'A', parent, copy_count = 1) |
Re-running replaces only the variant at the same scope
(mode = "replace_scope"), so successive common / line-A / line-B calls each
keep the others: the per-copy fallback that makes crossbred breeding values
correct depends on all of them standing. It also means changing
parent_origin on a re-run adds a variant rather than replacing one —
the two are in a containment relation and both apply, to different copies.
That is legal and rarely intended, so the function warns on exactly that
case.
See also
define_genome_effects() for dominance and A x A effects;
extract_genetic_variance() to measure what a population has;
define_trait(), define_effect_cov_matrix(),
define_genome_effect_terms() for known coefficients,
remove_generated_effects(), extract_allele_freq().
Examples
# A small in-memory population with two founder lines
set.seed(1)
pop <- open_pop(pop_name = "dae_example", db_name = ":memory:") |>
define_genome(n_loci = 200, n_chr = 2, chr_len_Mb = 100) |>
define_founder_haplotypes(n_haplotypes = 100, line_name = "Duroc") |>
define_founder_haplotypes(n_haplotypes = 100, line_name = "Landrace")
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpvvfSjW/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> Opened in-memory population 'dae_example'
#> Defined genome: 200 loci across 2 chromosomes | chr lengths (Mb): all equal to 100 Mb
#> Tables written: genome_meta, genome_map, ind_haplotype, ind_genotype, ind_crossover, chr_inheritance, chr_recombination, genome_effects, genome_effect_members, genome_effect_member_origins
#> Views created: genome_effect_terms, genome_effect_loci
#> Generating 100 founder haplotypes (method = "uniform")...
#> Created founder_haplotypes for line 'Duroc' (100 haplotypes x 200 loci)
#> Generating 100 founder haplotypes (method = "uniform")...
#> Created founder_haplotypes for line 'Landrace' (100 haplotypes x 200 loci)
pop <- pop |>
get_table("founder_haplotypes") |>
dplyr::filter(line_name == "Duroc") |>
add_founders(n_males = 20, n_females = 40, line_name = "Duroc")
#> Added 60 founders (20 males, 40 females) to line 'Duroc'
for (t in c("ADG", "BW", "FCR", "LMA", "IMF", "BF")) pop <- define_trait(pop, t)
#> Added trait 'ADG'.
#> Added trait 'BW'.
#> Added trait 'FCR'.
#> Added trait 'LMA'.
#> Added trait 'IMF'.
#> Added trait 'BF'.
# The Duroc founder pool as the base (the default would pool both lines)
duroc <- get_table(pop, "founder_haplotypes") |>
dplyr::filter(line_name == "Duroc")
# The p a base gives, before committing to it
head(extract_allele_freq(duroc))
#> # A tibble: 6 × 3
#> locus_id locus_name allele_freq
#> <int> <chr> <dbl>
#> 1 1 Locus_1 0.77
#> 2 2 Locus_2 0.42
#> 3 3 Locus_3 0.78
#> 4 4 Locus_4 0.54
#> 5 5 Locus_5 0.72
#> 6 6 Locus_6 0.98
# One trait: every locus on chromosome 1 is a QTL. G writes the target to
# trait_var_comp and calibrates to it in one transaction.
pop <- pop |>
get_table("genome_meta") |>
dplyr::filter(chr_name == "1") |>
define_additive_effects("ADG", G = 0.25, base_tbl = duroc, seed = 11)
#> The founder pool (pool expectation under random pairing) sees relative spectrum [0.826, 0.826] of the target (sampling LD of 100 haplotypes).
#> Set additive effects for 100 QTL on trait 'ADG' (base: founder_haplotypes [1 filter]; scope: all lines, both parents' copies): exact under the genic anchor; delivered variance 0.25.
# Two correlated traits on one shared QTL set: both variances and the
# correlation are delivered exactly
G <- matrix(c(0.25, 0.10, 0.10, 0.30), 2, 2,
dimnames = list(c("BW", "FCR"), c("BW", "FCR")))
pop <- pop |>
get_table("genome_meta") |>
define_additive_effects(c("BW", "FCR"), G = G, base_tbl = duroc, seed = 12)
#> The founder pool (pool expectation under random pairing) sees relative spectrum [1.01, 1.12] of the target (sampling LD of 100 haplotypes).
#> Set correlated additive effects for traits: BW, FCR (method: shared; base: founder_haplotypes [1 filter]; scope: all lines, both parents' copies): exact under the genic anchor; delivered [BW,BW = 0.25; BW,FCR = 0.1; FCR,FCR = 0.3].
# A target stored beforehand is read back when G is omitted
pop <- pop |> define_effect_cov_matrix("additive", 0.4, trait_name = "LMA")
#> Stored 'additive' covariance matrix for: LMA.
pop <- pop |>
get_table("genome_meta") |>
define_additive_effects("LMA", base_tbl = duroc, seed = 13)
#> The founder pool (pool expectation under random pairing) sees relative spectrum [1.21, 1.21] of the target (sampling LD of 100 haplotypes).
#> Set additive effects for 200 QTL on trait 'LMA' (base: founder_haplotypes [1 filter]; scope: all lines, both parents' copies): exact under the genic anchor; delivered variance 0.4.
# Exact in the founders themselves, linkage disequilibrium included. The
# founders add_founders() created are the ind_meta rows with no parents;
# the loci come from the pipe, the individuals from base_tbl.
founders <- get_table(pop, "ind_meta") |>
dplyr::filter(is.na(id_parent_1), line_name == "Duroc")
pop <- pop |>
get_table("genome_meta") |>
define_additive_effects("IMF", G = 0.1, anchor = "realised",
base_tbl = founders, seed = 14)
#> Set additive effects for 200 QTL on trait 'IMF' (base: ind_meta [2 filters]; scope: all lines, both parents' copies): exact under the realised anchor; delivered variance 0.1.
pop |>
get_table("ind_meta") |>
extract_genetic_variance(trait_name = "IMF")
#> Realised genetic covariances of the 60 selected individuals (frequencies and regressions are the cohort's own).
#> # A tibble: 3 × 7
#> effect_name trait_name_1 trait_name_2 cov_value n_ind decomposition anchor
#> <chr> <chr> <chr> <dbl> <int> <chr> <chr>
#> 1 additive IMF IMF 0.1 60 full reali…
#> 2 between_compon… IMF IMF 0 60 full reali…
#> 3 total IMF IMF 0.1 60 full reali…
# Crossbreeding: a common variant centred on the pooled founder table
# (named on purpose, so no Wahlund warning), plus one variant per line,
# each centred on that line's own pool and reading the common target
gm <- pop |> get_table("genome_meta") |> dplyr::filter(chr_name == "2")
pop <- gm |> define_additive_effects("BF", G = 0.2,
base_tbl = get_table(pop, "founder_haplotypes"), seed = 15)
#> The founder pool (pool expectation under random pairing) sees relative spectrum [0.889, 0.889] of the target (sampling LD of 200 haplotypes).
#> Set additive effects for 100 QTL on trait 'BF' (base: founder_haplotypes; scope: all lines, both parents' copies): exact under the genic anchor; delivered variance 0.2.
pop <- gm |> define_additive_effects("BF", line_name = "Duroc", seed = 16)
#> Set additive effects for 100 QTL on trait 'BF' (base: founder_haplotypes [1 filter]; scope: line Duroc, both parents' copies): exact under the genic anchor; delivered variance 0.2.
#> Line-specific effects are centred on line Duroc's own base, so they add no difference between line means. For differences between lines, use common effects centred on one reference line (see ?define_additive_effects).
pop <- gm |> define_additive_effects("BF", line_name = "Landrace", seed = 17)
#> Set additive effects for 100 QTL on trait 'BF' (base: founder_haplotypes [1 filter]; scope: line Landrace, both parents' copies): exact under the genic anchor; delivered variance 0.2.
#> Line-specific effects are centred on line Landrace's own base, so they add no difference between line means. For differences between lines, use common effects centred on one reference line (see ?define_additive_effects).
# Evaluate the breeding values
pop <- pop |> get_table("ind_meta") |> add_tgv(c("ADG", "BW", "FCR", "BF"))
#> Computed TGV for 60 individuals on trait 'ADG' (additive).
#> Computed TGV for 60 individuals on trait 'BW' (additive).
#> Computed TGV for 60 individuals on trait 'FCR' (additive).
#> Computed TGV for 60 individuals on trait 'BF' (additive).
close_pop(pop)