library(simplePHENOTYPES)
library(dplyr)
library(tibble)15 Contributions and cross usefulness
Truncation selection keeps the best candidates and discards the rest. Over cycles that erodes diversity and, with it, future gain. This chapter asks two questions a breeder faces before mating: how much should each candidate contribute to the next generation, and which crosses are worth making? The first is answered by optimum contribution selection, the second by the usefulness of a cross. Both are computed on simulated maize lines, so every expectation can be checked against realized progeny.
15.1 Worked example
15.1.1 Load the package and candidates
The candidates are the first 40 inbred lines of the bundled maize panel (see A first simulation), with an additive trait of 50 causal markers. as_population() turns the numeric genotype table into a Population, the object the crossing and selection functions operate on.
data("SNP55K_maize282_maf04", package = "simplePHENOTYPES")
geno <- as_numeric(
SNP55K_maize282_maf04,
to_r = TRUE, to_file = FALSE, verbose = FALSE
)
pop <- as_population(geno, individuals = 1:40)
sim <- simulate_phenotype(pop, h2 = 0.5, seed = 11) |>
additive(n_qtn = 50)
print(sim)#> <phenotype_sim> (realized · long format)
#> Genotypes: <Population: founder> Traits: 1 Architecture: independent Seed: 11
#> Variance partition (proportions of V_P):
#> additive 0.50 (50 QTNs, geometric)
#> residual 0.50
#> Requested genetic share = 0.50 realized H² = 0.70
The realized genetic-to-phenotypic variance ratio differs from the requested 0.5 for the reasons explained in the first chapter: the sample covariance between the genetic and residual components is free, and with only 40 individuals it can be large.
15.1.3 Optimum contributions
optimum_contribution() chooses a contribution vector c, one value per candidate, that is non-negative and sums to one. It maximizes the expected merit of the parents minus a penalty on their group coancestry. The merit is the simulated breeding value by default. The penalty lambda sets the trade-off.
oc <- optimum_contribution(sim, merit = "bv", lambda = 2)
print(oc)#> <ocs> optimum contribution selection
#> Parents with contribution > 0: 14 of 40
#> Expected merit (c'g): 0.7842 Group coancestry (0.5 c'Gc): 0.08069
#> Penalty lambda: 2
#> Top contributions:
#> A680 0.149
#> A6 0.130
#> B52 0.099
#> 38-11 0.096
#> B105 0.092
#> B46 0.090
#> ... (8 more)
The printed summary reports the expected merit c'g, the group coancestry 0.5 c'Gc, and the largest contributions. We confirm the two defining properties of the solution directly: the contributions sum to one and the reported coancestry equals the quadratic form computed from G.
cc <- oc$contributions
bv <- genetic_values(sim)[, 1]
tibble(
quantity = c("Sum of contributions", "Expected merit", "Group coancestry"),
reported = c(sum(cc), oc$merit, oc$coancestry),
recomputed = c(
sum(cc),
sum(cc * bv[names(cc)]),
0.5 * drop(crossprod(cc, G %*% cc))
)
)| quantity | reported | recomputed |
|---|---|---|
| Sum of contributions | 1.0000000 | 1.0000000 |
| Expected merit | 0.7841941 | 0.7841941 |
| Group coancestry | 0.0806949 | 0.0806949 |
15.1.4 The gain-diversity frontier
Repeating the optimization over a range of penalties traces the frontier between expected merit and group coancestry. At lambda = 0 nothing penalizes relatedness, so all weight goes to the single best candidate. Larger penalties spread the contributions over more parents at a cost in expected merit.
frontier <- lapply(c(0, 0.5, 2, 10, 50), function(lambda) {
o <- optimum_contribution(sim, lambda = lambda)
tibble(
lambda = lambda, n_parents = o$n_parents,
merit = o$merit, coancestry = o$coancestry
)
}) |>
bind_rows()
frontier| lambda | n_parents | merit | coancestry |
|---|---|---|---|
| 0.0 | 1 | 0.9533598 | 1.2086537 |
| 0.5 | 7 | 0.8791137 | 0.1811001 |
| 2.0 | 14 | 0.7841941 | 0.0806949 |
| 10.0 | 29 | 0.5133659 | 0.0222279 |
| 50.0 | 40 | 0.1295176 | 0.0012952 |
Breeders more often think in terms of a coancestry they are willing to accept than in terms of a penalty. target_coancestry finds the penalty whose solution has exactly that group coancestry, by bisection on lambda.
tg <- optimum_contribution(sim, target_coancestry = 0.3)
print(tg)#> <ocs> optimum contribution selection
#> Parents with contribution > 0: 4 of 40
#> Expected merit (c'g): 0.9142 Group coancestry (0.5 c'Gc): 0.3
#> Penalty lambda: 0.161
#> Top contributions:
#> A6 0.394
#> A680 0.245
#> A679 0.194
#> 38-11 0.167
15.1.5 From contributions to parents
Contributions are fractions; a mating plan needs individuals. sample_parents() fills n parent slots. The default "allocate" method gives each candidate the whole-number part of n times its contribution, then assigns the leftover slots to the largest remainders. The "multinomial" method draws slots at random with replacement, weighted by the contributions.
parents <- sample_parents(oc, pop, n = 20, seed = 12)
print(parents)#> <Population>
#> Individuals: 20 Markers: 10650 Chromosomes: 10
#> Genetic map: 1497 cM total span (109-219 cM per chromosome)
#> Origin: founder
#> Pedigree: 11 recorded individuals; generation 0
#> IDs: A6, B46, B109, A679, A680, B105, ... (14 more)
slots <- attr(parents, "source") |>
count(id, name = "slots") |>
mutate(
realized = slots / 20,
optimized = oc$contributions[id]
) |>
arrange(desc(optimized))
slots| id | slots | realized | optimized |
|---|---|---|---|
| A680 | 3 | 0.15 | 0.1490808 |
| A6 | 3 | 0.15 | 0.1298625 |
| B52 | 2 | 0.10 | 0.0990912 |
| 38-11 | 2 | 0.10 | 0.0962708 |
| B105 | 2 | 0.10 | 0.0922168 |
| B46 | 2 | 0.10 | 0.0904049 |
| A679 | 2 | 0.10 | 0.0831612 |
| A188 | 1 | 0.05 | 0.0733524 |
| B75 | 1 | 0.05 | 0.0597300 |
| AB28A | 1 | 0.05 | 0.0480971 |
| B109 | 1 | 0.05 | 0.0312565 |
The source attribute records which candidate fills each slot. Every realized contribution is within one slot of its optimized value. The group coancestry of the realized set is not guaranteed to equal the optimum, because coancestry is quadratic in the contributions. The comparison below computes it for both methods; the multinomial draw usually lands further from the optimum because its counts carry sampling noise.
multinomial <- sample_parents(oc, pop, n = 20, seed = 12, method = "multinomial")
realized_coancestry <- function(parents) {
counts <- table(attr(parents, "source")$id)
c_realized <- setNames(rep(0, nrow(G)), rownames(G))
c_realized[names(counts)] <- as.numeric(counts) / sum(counts)
0.5 * drop(crossprod(c_realized, G %*% c_realized))
}
tibble(
set = c("Optimized contributions", "allocate, n = 20", "multinomial, n = 20"),
coancestry = c(
oc$coancestry,
realized_coancestry(parents),
realized_coancestry(multinomial)
)
)| set | coancestry |
|---|---|
| Optimized contributions | 0.0806949 |
| allocate, n = 20 | 0.0964604 |
| multinomial, n = 20 | 0.1464855 |
The returned Population can be mated with cross() or mating_design(); repeated copies of a parent receive distinct identifiers because the crossing functions require unique names.
15.1.6 Which crosses to make
Optimum contributions decide who should be a parent. Usefulness decides which pairs to cross. cross_usefulness() simulates a progeny family for each candidate pair, scores the progeny on the trait’s additive effects, and reports the family mean, its genetic standard deviation, and the usefulness \(U = \mu + i\,\sigma\), where \(i\) is the selection intensity for the best select_top fraction.
For the doubled-haploid and selfing schemes the family is derived from a single F1 of the cross, which is only well defined for inbred parents. The maize lines carry a little residual heterozygosity, so we take the first eight lines and remove the few markers at which any of them is heterozygous. This is a teaching convenience, not a recommendation for real data; it also keeps the candidates diverse, which matters because the scoring effects are standardized over the candidates themselves.
lines <- names(geno)[6:13]
inbred_geno <- filter_geno(
geno[, c(names(geno)[1:5], lines)],
hets = "remove", verbose = FALSE
)
inbred <- as_population(inbred_geno)
print(inbred)#> <Population>
#> Individuals: 8 Markers: 10276 Chromosomes: 10
#> Genetic map: 1497 cM total span (109-219 cM per chromosome)
#> Origin: founder
#> Pedigree: 8 recorded individuals; generation 0
#> IDs: 4226, 4722, 33-16, 38-11, A188, A239, ... (2 more)
The usefulness is computed on the scoring model of a simulation built on these eight lines: the simulation supplies the causal markers and their fixed additive effects.
sim8 <- simulate_phenotype(inbred, h2 = 0.5, seed = 11) |>
additive(n_qtn = 50)
u <- cross_usefulness(sim8, scheme = "dh", n_progeny = 100, seed = 13)
head(u)| parent1 | parent2 | mean | sd | usefulness |
|---|---|---|---|---|
| 4722 | 38-11 | 0.1405273 | 0.6502431 | 1.2816931 |
| 4226 | 38-11 | 0.5902997 | 0.3389489 | 1.1851492 |
| 38-11 | A441-5 | 0.5611812 | 0.3361183 | 1.1510632 |
| 38-11 | A554 | -0.0041590 | 0.6473470 | 1.1319242 |
| 38-11 | A188 | -0.0240380 | 0.6353681 | 1.0910225 |
| 33-16 | 38-11 | -0.1903139 | 0.6550748 | 0.9593314 |
attr(u, "intensity")#> [1] 1.754983
All 28 pairs of the eight lines were evaluated, ranked best first. The intensity attribute is the normal-theory value for the top ten percent. A parent-mean ranking would use only the midparent column of the comparison below; usefulness adds the family’s genetic spread, which is why crosses with a lower mean can rank above crosses with a higher one.
template <- template_effects(sim8)
parent_value <- additive_value(inbred, template$qtn, template$a)
u |>
mutate(midparent = (parent_value[parent1] + parent_value[parent2]) / 2) |>
select(parent1, parent2, midparent, mean, sd, usefulness) |>
head(8)| parent1 | parent2 | midparent | mean | sd | usefulness |
|---|---|---|---|---|---|
| 4722 | 38-11 | 0.0873095 | 0.1405273 | 0.6502431 | 1.2816931 |
| 4226 | 38-11 | 0.5795739 | 0.5902997 | 0.3389489 | 1.1851492 |
| 38-11 | A441-5 | 0.5372545 | 0.5611812 | 0.3361183 | 1.1510632 |
| 38-11 | A554 | -0.0402122 | -0.0041590 | 0.6473470 | 1.1319242 |
| 38-11 | A188 | -0.0938098 | -0.0240380 | 0.6353681 | 1.0910225 |
| 33-16 | 38-11 | -0.1384622 | -0.1903139 | 0.6550748 | 0.9593314 |
| 4226 | 4722 | -0.1168312 | -0.1035128 | 0.5747727 | 0.9052037 |
| 38-11 | A239 | 0.3939016 | 0.3819834 | 0.2929137 | 0.8960421 |
15.1.7 Check the usefulness against realized progeny
We simulate the top-ranked cross ourselves: make the F1, derive 100 doubled haploids, score them with the same additive effects, and compare the usefulness to the mean of the best ten progeny.
best <- u[1, ]
f1 <- cross(inbred[best$parent1], inbred[best$parent2], n = 1, seed = 14)
dh <- double_haploid(f1, n = 100, seed = 15)
progeny <- additive_value(dh, template$qtn, template$a)
top10 <- mean(sort(progeny, decreasing = TRUE)[1:10])
tibble(
quantity = c(
"Family mean", "Family genetic SD",
"Usefulness (normal theory)", "Mean of the best 10 of 100 progeny"
),
predicted = c(best$mean, best$sd, best$usefulness, NA),
simulated = c(mean(progeny), sd(progeny), NA, top10)
)| quantity | predicted | simulated |
|---|---|---|
| Family mean | 0.1405273 | 0.0441158 |
| Family genetic SD | 0.6502431 | 0.7043900 |
| Usefulness (normal theory) | 1.2816931 | NA |
| Mean of the best 10 of 100 progeny | NA | 1.1337496 |
The two family simulations use different seeds, so the means and standard deviations differ by Monte Carlo error. The usefulness and the realized top-fraction mean differ for a second reason explained below.
The "selfcross" scheme derives near-inbred lines by repeated selfing instead of doubling the F1 gametes; its results are usually close to the doubled-haploid ranking for an additive trait.
cross_usefulness(
sim8, pairs = u[1:3, c("parent1", "parent2")],
scheme = "selfcross", n_progeny = 100, generations = 5, seed = 13
)| parent1 | parent2 | mean | sd | usefulness |
|---|---|---|---|---|
| 4722 | 38-11 | 0.1754689 | 0.6419310 | 1.302047 |
| 4226 | 38-11 | 0.6213227 | 0.3460395 | 1.228616 |
| 38-11 | A441-5 | 0.5327404 | 0.3127582 | 1.081626 |
15.2 Assumptions and theory
15.2.1 The optimum contribution problem
Optimum contribution selection maximizes
\[ c'g - \frac{\lambda}{2}\, c'Gc \qquad\text{subject to}\qquad c \ge 0,\; \mathbf{1}'c = 1, \tag{15.1}\]
where \(c\) is the vector of contributions, \(g\) the vector of merits, \(G\) the genomic relationship matrix, and \(\lambda\) the penalty. The group coancestry of the chosen parents is \(\tfrac12 c'Gc\). The equation map records the reference as Meuwissen 1997 J Anim Sci 75:934–940 (verified; page unverified). The implementation in optimum_contribution() and the internal optimizer .frank_wolfe() is a package definition: an away-step Frank-Wolfe method on the simplex, with the penalty tuned by bisection in .tune_lambda() when a target coancestry is requested. With direction = "low" the merit is negated.
Because the contributions sum to one, adding a constant to every merit does not change the solution. The merit scale does matter relative to lambda: doubling all merits halves the effective penalty, which is why the frontier should be read as a curve over lambda, not as a set of universal values.
15.2.2 The genomic relationship matrix
g_matrix() computes
\[ G = \frac{ZZ'}{2\sum_j p_j(1-p_j)}, \qquad Z = M - 2p, \tag{15.2}\]
with \(M\) the individuals-by-markers gene-content matrix (0, 1, 2) and \(p_j\) the frequency of the counted allele at marker \(j\). This is VanRaden 2008 J Dairy Sci 91:4414–4423, method 1 (paper verified; page unverified in the equation map). Markers monomorphic in the current set are dropped. The optional ridge argument blends \(G\) with the identity; that blend is a package extension for numerical positive definiteness, not VanRaden’s blend with a pedigree matrix.
15.2.3 Usefulness
The usefulness criterion is
\[ U = \mu + i\,\sigma, \tag{15.3}\]
with \(\mu\) the family mean and \(\sigma\) the family genetic standard deviation of the additive value, both realized by the crossing engine from n_progeny simulated progeny, and \(i = \varphi(\Phi^{-1}(1-p))/p\) the standard normal-theory selection intensity for the fraction \(p\) in select_top. The equation map cites Zhong & Jannink 2007 Genetics 177:567–576 and Lehermeier et al. 2017 Genetics 207:1651–1661 (verified; pages unverified) for the criterion and Falconer & Mackay 1996 (author-year, no page asserted) for the intensity.
Two facts explain why the usefulness need not equal the realized top-fraction mean. First, \(i\) is the infinite-population value; the mean of the top 10 of 100 ordered normal values is slightly smaller. Second, the family’s genetic values are a sum over a few dozen loci with geometric effects, so they are not exactly normal; a family dominated by a few large effects is discrete and skewed, and \(U\) can then exceed the family maximum. U is therefore a ranking criterion under a normality assumption, not an exact prediction.
15.2.4 Scores are on a fixed scale
The progeny scores in cross_usefulness() and the additive_value() checks above use the additive effects frozen from the template simulation, \(g_i = \sum_j x_{ij} e_j\) with \(e_j\) already multiplied by the layer’s \(\sqrt{\pi}/\mathrm{sd}\) factor (.additive_model(), a package definition mirroring .genetic_matrix()). Scoring a family by re-running simulate_phenotype() on it would re-standardize the genetic component within each family and destroy the comparability of means and standard deviations between crosses.
15.3 Implementation and practical limits
The optimizer, the relationship matrix, and the usefulness calculations are deterministic R code; the random steps are the simulated meioses of cross_usefulness() and the slot ordering or draws of sample_parents(), all seeded through R.
optimum_contribution()takes exactly one oflambda,target_coancestryormax_coancestry. A target above the coancestry of the unconstrained optimum cannot be reached by a non-negative penalty; the function warns and useslambda = 0.- The default merit
"bv"is the transmitting average effect \(\alpha_j = a_j + d_j(q_j - p_j)\), computed from the simulation’s own effects. It is refused for models with an epistasis layer; pass a numericmeritthere, for example estimated breeding values frompredict_ebv(). g_matrix()refuses missing genotypes and any coding other than-1,0,1; impute first withas_numeric(impute = ).sample_parents()with"allocate"can give a small contributor no slot whennis small. Increasenif every contributor must appear.cross_usefulness()evaluates all pairs by default; the count grows with the square of the number of candidates. Passpairsfor large panels.- Only the additive component is scored; dominance and epistasis layers are ignored, and the function warns when a parent is heterozygous at a scoring locus under the
"dh"or"selfcross"schemes.
A common pipeline is to compute contributions with optimum_contribution(), fill the parent slots with sample_parents(), and then rank the pairs among those parents with cross_usefulness() before calling mate(). The breeding schemes chapter shows how such a cycle is repeated.