Updates on genomic selection

Day 3: Genomic selection models enriched with environmental data


Saulo Chaves

Assistant Professor

Data Science for Plant Breeding Lab

Piracicaba, September 3rd

Objectives

  • To show how environmental data can be incorporated into genomic prediction models
  • To run multi-environment models enriched with environmental data and validate them in different scenarios

Environmics

Genotype-by-environment interaction

Genotype-by-environment interaction

Genotype-by-environment interaction

GEI from the environmental perspective

Environmics

An omics approach that uses the full spectrum of environmental variations to understand and predict G × E interaction — Fritsche-Neto et al. (2025)

Environmental covariates

Data extrinsic to the tested genotypes

Environmic marker

An index vector derived from environmental covariates (ECs) which reflects the potential expression of a phenotype in a given space and time — Marcatti & Resende (2026)

Environmic marker

Marker engineering

Environmics

A new paradigm

Environmics

Plant breeding triangle

Environmics + Genomics

Reaction norm models

Proposed by Jarquin et al. (2014) to surpass dimensionality issues

  • Baseline multi-environment GBLUP model:

\[ \bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol e + \boldsymbol Z_2 \boldsymbol g + \boldsymbol \varepsilon \]

  • \(\boldsymbol e \to\) Fixed effect of environment
  • \(\boldsymbol g \to\) Random main genetic effect
    • \(\boldsymbol g \sim \mathcal N \left(\boldsymbol 0, \boldsymbol K_g \sigma^2_g\right)\)
  • \(\boldsymbol \varepsilon \to\) IID residual

Reaction norm models

  • Research question: How molecular markers interact with environmental covariables?
  • First, are the environments really independent?
  • Environmic kernel:

\[ \boldsymbol{\Omega} = \frac{\boldsymbol{V}\boldsymbol{V}^\prime}{q} \]

  • \(\boldsymbol V \to\) matrix of environmental markers (scaled and centred)
  • \(q \to\) number of environmental markers (columns of \(\boldsymbol V\))

Reaction norm models

\[ \bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol \omega + \boldsymbol Z_2 \boldsymbol g + \boldsymbol \varepsilon \]

  • Environmental covariable main effect:

\[ \boldsymbol \omega \sim \mathcal N\left(\boldsymbol 0, \boldsymbol \Omega \sigma^2_e\right) \]

  • Prediction of new environments

Reaction norm models

The ECs may not fully describe differences across environments, perhaps because some relevant ECs were not measured or because of model miss-specification (e.g., non-linear effects of ECs on the trait of interest). Similarly, because of imperfect linkage disequilibrium (LD) between markers and genes at causal loci or because of model misspecification (e.g., interactions between alleles that are unaccounted for), the regression on markers may not fully describe genetic differences among lines.” — Jarquin et al. (2014)

Reaction norm models

  • “Residual” environmental effects:

\[ \bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol \omega + \boldsymbol Z_2 \boldsymbol e + \boldsymbol Z_3 \boldsymbol g + \boldsymbol \varepsilon \]

\[ \boldsymbol e \sim \mathcal{N} \left(\boldsymbol 0, \boldsymbol I \sigma^2_e\right) \]

  • What about the “residual” genetic effects?

Reaction norm models

Genotype-by-environmental covariate interaction (first order multiplicative interaction)

\[ gw_{ik} = g_i \times \omega_k \]

\[ \operatorname{Cov} \left(gw_{ik}, gw_{i^\prime k^\prime}\right) = G_{ii^\prime} \times \Omega_{kk^\prime} \]

  • Interaction built with the cell-by-cell (Hadamard) product between the entries of \(\boldsymbol G\) and \(\boldsymbol \Omega\)

\[ \bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol \omega + \boldsymbol Z_2 \boldsymbol e + \boldsymbol Z_3 \boldsymbol g + \boldsymbol Z_3 \boldsymbol {g\omega} + \boldsymbol \varepsilon \]

\[ \boldsymbol {g\omega} \sim \mathcal{N} \left[\boldsymbol 0, \left(\boldsymbol Z_3 \boldsymbol G \boldsymbol Z_3^\prime\right) \odot \left(\boldsymbol Z_1 \boldsymbol \Omega \boldsymbol Z_1^\prime\right) \sigma^2_{g\omega}\right] \]

Reaction norm models

Genotype-by-environment interaction

\[ \bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol \omega + \boldsymbol Z_2 \boldsymbol e + \boldsymbol Z_3 \boldsymbol g + \boldsymbol Z_3 \boldsymbol {g\omega} + \boldsymbol Z_4 \boldsymbol {ge} + \boldsymbol \varepsilon \]

\[ \boldsymbol {ge} \sim \mathcal{N} \left[\boldsymbol 0, \left(\boldsymbol Z_3 \boldsymbol G \boldsymbol Z_3^\prime\right) \odot \left(\boldsymbol Z_2 \boldsymbol Z_2^\prime\right) \sigma^2_{ge}\right] \]

  • “Residual” genotype-by-environment interaction

Reaction norm models

Cross-validations

CV0: predict the performance of tested genotypes in untested environments

Cross-validations

CV00: predict the performance of untested genotypes in untested environments (most challenging scenario of them all!)

Cross-validations

Overview

Practical session

The dataset

G2F

Phenotypic data

library(tidyverse)

dat = readRDS("../Data/G2F.RDS")
str(dat)
'data.frame':   85949 obs. of  7 variables:
 $ gen      : chr  "B37/C103" "B37/H95" "B73/MO17" "B73/PHG35" ...
 $ year     : int  2014 2014 2014 2014 2014 2014 2014 2014 2014 2014 ...
 $ loc      : chr  "GAH1" "GAH1" "GAH1" "GAH1" ...
 $ env      : chr  "GAH1_2014" "GAH1_2014" "GAH1_2014" "GAH1_2014" ...
 $ blue     : num  4.69 5.98 6.29 9.73 8.05 ...
 $ std.error: num  0.828 0.828 0.828 0.828 0.828 ...
 $ wt       : num  1.91 1.91 1.91 1.91 1.91 ...

Genomic data

geno.code = readRDS(file = "../Data/mrk_filtered.RDS")
str(geno.code)
 int [1:5899, 1:2105] 1 0 0 1 1 1 1 0 0 0 ...
 - attr(*, "dimnames")=List of 2
  ..$ : chr [1:5899] "01CSI6/LH287" "01DIB2/LH287" "01DIB2/PHP02" "2369/DK3IIH6" ...
  ..$ : chr [1:2105] "S1_1007742" "S1_1020677" "S1_2018002" "S1_2275970" ...
any(is.na(geno.code))
[1] FALSE

Data management

dat = droplevels(dat[which(
  dat$year > 2021 &
    dat$loc %in% c("GAH1", "IAH1", "IAH2", "ILH1", "INH1", "MNH1", "MOH1", "MOH2")
), ])
dat = dat[which(dat$gen %in% rownames(geno.code)),]
set.seed(7)
dat = dat[which(dat$gen %in% sample(unique(dat$gen),150)),]
geno.code = geno.code[which(rownames(geno.code) %in% dat$gen),]
all(unique(dat$gen) %in% rownames(geno.code))
[1] TRUE

Environmental data

Soil data: collected in loco

soil = read.csv(file = "../Data/Soil.csv")
str(soil)
'data.frame':   186 obs. of  36 variables:
 $ Year                      : int  2015 2015 2015 2015 2015 2015 2015 2015 2015 2015 ...
 $ Env                       : chr  "IAH1_2015" "IAH3_2015" "IAH4_2015" "INH1_2015" ...
 $ LabID                     : chr  "UW Soil & Plant Analysis Lab" "UW Soil & Plant Analysis Lab" "Soil & Forage Analysis Lab" "UW Soil & Plant Analysis Lab" ...
 $ Date.Received             : chr  "" "" "" "" ...
 $ Date.Reported             : chr  "5/27/2015" "5/27/2015" "12/7/2015" "6/2/2015" ...
 $ E.Depth                   : int  NA NA NA NA NA NA NA NA NA NA ...
 $ X1.1.Soil.pH              : num  5.3 6.5 6.4 6.1 6.5 6.2 6.4 7.1 5.8 6.4 ...
 $ WDRF.Buffer.pH            : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X1.1.S.Salts.mmho.cm      : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Texture.No                : int  NA NA NA NA NA NA NA NA NA NA ...
 $ Organic.Matter.LOI..      : num  3.2 3.4 4.4 2.9 2.1 4.2 2.3 1.8 1.6 2 ...
 $ Nitrate.N.ppm.N           : num  NA NA NA NA NA NA NA NA NA NA ...
 $ lbs.N.A                   : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Potassium.ppm.K           : num  118 114 95 86 145 109 104 93.5 330 455 ...
 $ Sulfate.S.ppm.S           : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Calcium.ppm.Ca            : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Magnesium.ppm.Mg          : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Sodium.ppm.Na             : num  NA NA NA NA NA NA NA NA NA NA ...
 $ CEC.Sum.of.Cations.me.100g: num  NA NA NA NA NA NA NA NA NA NA ...
 $ X.H.Sat                   : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X.K.Sat                   : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X.Ca.Sat                  : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X.Mg.Sat                  : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X.Na.Sat                  : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Mehlich.P.III.ppm.P       : num  34 38 42 14 11 19 18 38.5 40 27 ...
 $ X..Sand                   : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X..Silt                   : num  NA NA NA NA NA NA NA NA NA NA ...
 $ X..Clay                   : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Texture                   : chr  "" "" "" "" ...
 $ BpH                       : num  6 6.5 6.4 6.5 6.7 6.5 6.6 NA 6.4 6.9 ...
 $ Zinc.ppm.Zn               : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Iron.ppm.Fe               : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Manganese.ppm.Mn          : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Copper.ppm.Cu             : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Boron.ppm.B               : num  NA NA NA NA NA NA NA NA NA NA ...
 $ Comments                  : chr  "" "" "" "" ...

Environmental data

More filtering

dat = dat[which(dat$env %in% soil$Env),]
length(unique(dat$env))
[1] 12
soil = soil[which(soil$Env %in% dat$env),]
dim(soil)
[1] 12 36
soil = soil[, -which(apply(soil, 2, function(x) any(is.na(x))))] 
soil = soil[, which(!colnames(soil) %in% c("LabID", "Date.Received", "Date.Reported",
                                           "E.Depth", "Texture", "Comments",
                                           "X.Na.Sat", "X.H.Sat", "Texture.No"))]

Environmental data

Environmental profile of each environment

Show code
ggplot(data = soil |> pivot_longer(colnames(soil[-c(1, 2)])), 
       aes(x = Env, y = value)) +
  facet_wrap(. ~ name, scales = "free_y") +
  geom_col(color = "black", fill = '#b2bc63') +
  theme_bw() +
  theme(axis.text.x = element_text(
    angle = 90,
    vjust = .5,
    hjust = 1
  ), axis.title.y = element_blank()) + 
  labs(x = "Environments")

Environmental data

Weather data: collected from nearby weather stations

weather = read.csv(file = "../Data/weather_seasons.csv")
str(weather)
'data.frame':   51098 obs. of  18 variables:
 $ Env               : chr  "DEH1_2015" "DEH1_2015" "DEH1_2015" "DEH1_2015" ...
 $ Date              : int  20150415 20150416 20150417 20150418 20150419 20150420 20150421 20150422 20150423 20150424 ...
 $ RH2M              : num  69.1 75 88.9 79.8 84.4 ...
 $ T2M_MAX           : num  16 19.1 22.1 23.4 14.3 ...
 $ ALLSKY_SFC_SW_DWN : num  15.9 25.1 16.4 24.6 18.1 ...
 $ T2MWET            : num  9.57 10.77 15.49 16.05 11.32 ...
 $ GWETTOP           : num  0.88 0.87 0.87 0.87 0.87 0.91 0.88 0.88 0.87 0.87 ...
 $ QV2M              : num  5.98 6.77 10.25 9.95 7.57 ...
 $ GWETPROF          : num  0.88 0.88 0.87 0.86 0.86 0.89 0.89 0.88 0.87 0.86 ...
 $ T2M_MIN           : num  9.23 7.72 11.17 12.42 10.57 ...
 $ T2MDEW            : num  6.57 8.44 14.51 14.12 9.98 ...
 $ PS                : num  102 103 102 101 102 ...
 $ T2M               : num  12.6 13.1 16.5 18 12.6 ...
 $ GWETROOT          : num  0.91 0.91 0.91 0.9 0.89 0.93 0.93 0.91 0.91 0.9 ...
 $ ALLSKY_SFC_PAR_TOT: num  83.1 126.4 87.1 126.2 94.4 ...
 $ WS2M              : num  2.7 2.54 3.07 1.41 5.13 5.52 3.9 4.32 4.09 4.37 ...
 $ ALLSKY_SFC_SW_DNI : num  6.85 27.66 14 28.05 8.14 ...
 $ PRECTOTCORR       : num  0.28 0.01 3.04 0.36 8.92 ...
weather = weather[which(weather$Env %in% dat$env),]
dim(weather)
[1] 2218   18

Environmental data

Environmental profile of environment “GAH1_2022”

Show code
weather$Date = lubridate::ymd(weather$Date)
ggplot(data = weather |> pivot_longer(colnames(weather[-c(1, 2)])) |>
         filter(Env == "GAH1_2022"),
       aes(x = Date, y = value)) +
  facet_wrap(name ~ ., scales = "free_y") +
  geom_point(color = '#10342d', alpha = .75) +
  geom_line(aes(group = 1), color = '#10342d', alpha = .8) +
  theme_bw() +
  theme(text = element_text(size = 15), axis.title.y = element_blank())

Environmental markers

Extracting environmental markers

# apply(weather[,-c(1,2)], 2, function(x) range(x)) 
envdata = list()
for (i in colnames(weather)[-c(1:2, 18)]) {
  temp = do.call(rbind, tapply(weather[,i], weather$Env, summary))
  colnames(temp) = c("min", "1qrt", "median", "mean", "3qrt", 'max')
  colnames(temp) = paste(i, colnames(temp), sep = "_")
  envdata[[i]] = temp
  rm(temp)
}
envdata = do.call(cbind, envdata)
envdata = cbind(envdata, 
                PRECTOT_sum = tapply(weather$PRECTOTCORR, weather$Env, sum),
                PRECTOT_mean = tapply(weather$PRECTOTCORR, weather$Env, mean),
                soil[match(rownames(envdata), soil$Env),-c(1,2)])

dim(envdata)
[1]  12 111

Environmental markers

Overview of the matrix of environmental markers

# any(is.na(envdata))
envdata = scale(envdata)  ## Very important!!!
Show code
library(ComplexHeatmap)
library(circlize)
col_fun = colorRamp2(c(min(envdata), 0, max(envdata)), c("blue4", "white", "red4"))
Heatmap(matrix = envdata,
        show_row_dend = FALSE, show_column_dend = FALSE, 
        column_order = colnames(envdata),
        column_split = ifelse(grepl("\\.", colnames(envdata)), "SOIL", gsub("_.*", "", colnames(envdata))),
        show_column_names = FALSE,
        column_title_rot = 45,
        border = TRUE,
        border_gp = gpar(col = "black", lty = 2),
        col = col_fun,
        show_heatmap_legend = FALSE)

Environmental markers

Environmental linear kernel

\[ \boldsymbol \Omega = \frac{\boldsymbol V \boldsymbol V^\prime}{q} \]

Emat = tcrossprod(envdata)/ncol(envdata)
str(Emat)
 num [1:12, 1:12] 2.26 1.57 -0.315 0.078 -0.736 ...
 - attr(*, "dimnames")=List of 2
  ..$ : chr [1:12] "GAH1_2022" "GAH1_2023" "IAH1_2022" "IAH1_2023" ...
  ..$ : chr [1:12] "GAH1_2022" "GAH1_2023" "IAH1_2022" "IAH1_2023" ...

Environmental markers

Environmental relationship matrix

Show code
col_fun = colorRamp2(c(min(Emat), 0, max(Emat)), c("blue4", "white", "green4"))
Heatmap(matrix = Emat,
        col = col_fun,
        show_heatmap_legend = TRUE,
        heatmap_legend_param = list(title = "Relationship"))

Environmental markers

Environmental relationship matrix

library(plotly)

pc = prcomp(Emat) 
pc.df = rbind(
  as.data.frame(pc$rotation) |> 
    rownames_to_column("ID") |> 
    mutate(comp = "load")
)

plot_ly(
  pc.df,
  x = ~ PC1,
  y = ~ PC2,
  z = ~ PC3
)  |> add_markers(
  text = ~ paste0("Environment: ", ID),
  hoverinfo = "text",
  marker = list(color = '#b2bc63', opacity = .62)
) |>  layout(scene = list(
  xaxis = list(
    title = paste0("PC1 (", round(pc$sdev[1]^2/sum(pc$sdev^2)*100,2), "%)"),
    showgrid = TRUE,
    range = c(min(c(
      range(pc.df$PC1), range(pc.df$PC2), range(pc.df$PC3)
    )), max(c(
      range(pc.df$PC1), range(pc.df$PC2), range(pc.df$PC3)
    )))
  ),
  yaxis = list(
    title = paste0("PC2 (", round(pc$sdev[2]^2/sum(pc$sdev^2)*100,2), "%)"),
    showgrid = TRUE,
    range = c(min(c(
      range(pc.df$PC1), range(pc.df$PC2), range(pc.df$PC3)
    )), max(c(
      range(pc.df$PC1), range(pc.df$PC2), range(pc.df$PC3)
    )))
  ),
  zaxis = list(
    title = paste0("PC3 (", round(pc$sdev[3]^2/sum(pc$sdev^2)*100,2), "%)"),
    showgrid = TRUE,
    range = c(min(c(
      range(pc.df$PC1), range(pc.df$PC2), range(pc.df$PC3)
    )), max(c(
      range(pc.df$PC1), range(pc.df$PC2), range(pc.df$PC3)
    )))
  )
))

GRM

\[ \boldsymbol{G} = \frac{\boldsymbol{\dot{W}}\boldsymbol{\dot{W}}^\prime}{2\sum_m p_m \left(1-p_m\right)} \]

library(AGHmatrix)
Gmat = Gmatrix(SNPmatrix = geno.code, method = "VanRaden")
Initial data: 
    Number of Individuals: 150 
    Number of Markers: 2105 

Missing data check: 
    Total SNPs: 2105 
     0 SNPs dropped due to missing data threshold of 0.5 
    Total of: 2105  SNPs 

MAF check: 
    No SNPs with MAF below 0 

Heterozigosity data check: 
    No SNPs with heterozygosity, missing threshold of =  0 

Summary check: 
    Initial:  2105 SNPs 
    Final:  2105  SNPs ( 0  SNPs removed) 
 
Completed! Time = 0.36  seconds 

GRM

Show code
library(ComplexHeatmap)
library(circlize)
col_fun = colorRamp2(c(-0.2, 0, 1.5), c("blue4", "white", "green4"))

Heatmap(
  Gmat,
  col = col_fun,
  show_row_names = F,
  show_column_names = F,
  heatmap_legend_param = list(title = "IBS")
)

Multi-environment GS model

Reaction norm model:

\[ \bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol \omega + \boldsymbol Z_2 \boldsymbol g + \boldsymbol Z_3 \boldsymbol {g\omega} + \boldsymbol \varepsilon \]

\[ \color{red}{\bar{\boldsymbol y} = \boldsymbol 1 \mu + \boldsymbol Z_1 \boldsymbol \omega + \boldsymbol Z_2 \boldsymbol g + \boldsymbol Z_3 \boldsymbol {g\omega}+ \boldsymbol Z_4 \boldsymbol {ge} + \boldsymbol \varepsilon} \]

  • \(\boldsymbol \omega \to\) Random effect of environmental covariates
  • \(\boldsymbol g \to\) Random main genetic effect
  • \(\boldsymbol {g\omega} \to\) Random effect of the genotype-by-environmental covariate interaction
  • \(\boldsymbol {ge} \to\) Random effect of the genotype-by-environment interaction
  • \(\boldsymbol \varepsilon \to\) IID residual

Multi-environment GS model

Expanding the data frame

nrow(dat)
[1] 1640
grid = expand.grid(
  gen = unique(dat$gen),
  env = unique(dat$env),
  stringsAsFactors = FALSE
)
dat = merge(grid, dat, by = c("gen", "env"), all.x = TRUE)
nrow(dat)
[1] 1800
dat$gen = factor(dat$gen, levels = rownames(Gmat), ordered = TRUE)
dat$env = factor(dat$env, levels = rownames(Emat), ordered = TRUE)
dat = dat[order(dat$env, dat$gen),]

Multi-environment GS model

Expanding the data frame

Show code
ggplot(data = dat, aes(
  x = reorder(env, blue),
  y = reorder(gen, blue),
  fill = blue
)) +
  geom_tile() +
  theme_minimal() +
  theme(axis.text.y = element_blank(), axis.text.x = element_text(angle = 90),
        legend.position = "top", legend.title = element_blank(), 
        text = element_text(size = 18)) +
  scale_fill_viridis_c(option = "inferno", na.value = "lightgrey") +
  labs(x = paste("Environments: ", length(unique(dat$env))),
       y = paste("Hybrid: ", length(unique(dat$gen))))

Multi-environment GS model

Incidence matrices

ZG = model.matrix(~-1 + gen, data = dat)
rownames(ZG) = dat$gen
colnames(ZG) = gsub("gen", "", colnames(ZG))

ZE = model.matrix(~-1 + env, data = dat)
rownames(ZE) = dat$env
colnames(ZE) = gsub("env", "", colnames(ZE))

Multi-environment GS model

\[ \boldsymbol K_g = \boldsymbol Z_g \boldsymbol G \boldsymbol Z_g^\prime \]

ZGZ = ZG %*% Gmat %*% t(ZG)
aux1 = rownames(ZGZ)
dim(ZGZ)
[1] 1800 1800

\[ \boldsymbol K_e = \boldsymbol Z_e \boldsymbol Z_e^\prime \]

ZEZ = tcrossprod(ZE)
dim(ZEZ)
[1] 1800 1800

\[ \boldsymbol K_\omega = \boldsymbol Z_\omega \boldsymbol \Omega \boldsymbol Z_\omega^\prime \]

ZOZ = ZE %*% Emat %*% t(ZE)
aux2 = rownames(ZOZ)
dim(ZEZ)
[1] 1800 1800

Multi-environment GS model

Show code
aux = do.call(c, lapply(split(dat, dat$env), nrow))
submat = ZOZ[1:sum(aux[1:6]), 1:sum(aux[1:6])]
col_fun = colorRamp2(c(min(submat), 0, max(submat)), c("blue4", "white", "green4"))

Heatmap(
  submat,
  show_row_dend = FALSE,
  show_row_names = FALSE,
  show_column_dend = FALSE,
  show_column_names = FALSE,
  column_split = rep(names(aux)[1:6], aux[1:6]),
  row_split = rep(names(aux)[1:6], aux[1:6]),
  col = col_fun,
  show_heatmap_legend = FALSE,
  border = TRUE,
  border_gp = gpar(col = "black", lty = 2),
  row_title_gp = gpar(fontsize = 10)
)

Multi-environment GS model

\[ \boldsymbol K_{ge} = \boldsymbol K_g \odot \boldsymbol K_e \]

GEI = ZGZ * ZEZ
aux3 = rownames(GEI)
dim(GEI)
[1] 1800 1800

Multi-environment GS model

\[ \boldsymbol K_{g\omega} = \boldsymbol K_g \odot \boldsymbol K_\omega \]

GEI_EC = ZGZ * ZOZ
aux4 = rownames(GEI_EC)
dim(GEI_EC)
[1] 1800 1800

Multi-environment GS model

Show code
submat = GEI_EC[1:sum(aux[1:5]),1:sum(aux[1:5])]
col_fun = colorRamp2(c(min(submat), 0, 3.8), c("blue4", "white", "green4"))

Heatmap(
  submat,
  show_row_dend = FALSE,
  show_row_names = FALSE,
  show_column_dend = FALSE,
  show_column_names = FALSE,
  column_split = rep(names(aux)[1:5], aux[1:5]),
  row_split = rep(names(aux)[1:5], aux[1:5]),
  col = col_fun,
  show_heatmap_legend = FALSE,
  border = TRUE,
  border_gp = gpar(col = "black", lty = 2)
)

Multi-environment GS model

Eigendecomposition (efficient computation)

ZGZ = eigen(ZGZ)
rownames(ZGZ$vectors) = aux1
ZEZ = eigen(ZEZ)
rownames(ZEZ$vectors) = aux2
ZOZ = eigen(ZOZ)
GEI = eigen(GEI)
rownames(GEI$vectors) = aux3
GEI_EC = eigen(GEI_EC)
rownames(GEI_EC$vectors) = aux4

Multi-environment GS model

Cholesky fatorization (even more efficient computation)

L_G = t(chol(Gmat + diag(x=0.0001, nrow = nrow(Gmat))))
L_E = t(chol(Emat))
X_g = ZG %*% L_G
X_e = ZE
X_ec = ZE %*% L_E
X_ge = model.matrix(~ -1 + X_g:dat$env)
X_ge_ec = do.call(cbind, lapply(1:ncol(X_ec), function(j) X_g * X_ec[, j]))

Multi-environment GS model

Factorized matrix of environmental relationship

Show code
Heatmap(
  L_E,
  show_row_dend = FALSE,
  show_row_names = FALSE,
  show_column_dend = FALSE,
  show_column_names = FALSE,
  row_order = rownames(L_E),
  column_order = colnames(L_E),
  col = col_fun,
  show_heatmap_legend = FALSE,
  row_split = rownames(L_E),
  row_title_gp = gpar(fontsize = 5)
)

Multi-environment GS model

Factorized covariance matrix of environmental covariates

Show code
Heatmap(
  X_ec,
  show_row_dend = FALSE,
  show_row_names = FALSE,
  show_column_dend = FALSE,
  show_column_names = FALSE,
  row_order = rownames(X_ec),
  column_order = colnames(X_ec),
  row_split = rep(names(aux), aux),
  col = col_fun,
  show_heatmap_legend = FALSE,
  row_title_gp = gpar(fontsize = 5)
)

Multi-environment GS model

Factorized genotype-by-environmental covariate kernel

Show code
submat = X_ge_ec
rownames(submat) = colnames(submat) = rep(names(aux), aux)
col_fun = colorRamp2(c(min(submat), 0, max(submat)), c("blue4", "white", "green4"))

Heatmap(
  X_ge_ec,
  show_row_dend = FALSE,
  show_row_names = FALSE,
  show_column_dend = FALSE,
  show_column_names = FALSE,
  row_order = 1:nrow(X_ge_ec),
  column_order = 1:ncol(X_ge_ec),
  col = col_fun,
  show_heatmap_legend = FALSE,
  # row_split = rep(names(aux), aux),
  # row_title_gp = gpar(fontsize = 5)
)

Multi-environment GS model

Models with only genotype-by-environmental covariate interaction

ETA1_kernel = list(ec = list(V = ZOZ$vectors, d = ZOZ$values, model = 'RKHS'),
                   hyb = list(V = ZGZ$vectors, d = ZGZ$values, model = 'RKHS'),
                   gei_ec = list(V = GEI_EC$vectors, d = GEI_EC$values, model = 'RKHS'))
ETA1_brr = list(ec = list(X = X_ec, model = 'BRR'),
                hyb = list(X = X_g, model = 'BRR'),
                gei_ec = list(X = X_ge_ec, model = 'BRR'))

Models with both interactions

ETA2_kernel = list(ec =  list(V = ZOZ$vectors, d = ZOZ$values, model = 'RKHS'),
                   hyb = list(V = ZGZ$vectors, d = ZGZ$values, model = 'RKHS'),
                   gei = list(V = GEI$vectors, d = GEI$values, model = 'RKHS'),
                   gei_ec = list(V = GEI_EC$vectors, d = GEI_EC$values, model = 'RKHS'))
ETA2_brr = list(ec = list(X = X_ec, model = 'BRR'),
                hyb = list(X = X_g, model = 'BRR'),
                gei = list(X = X_ge, model = 'BRR'),
                gei_ec = list(X = X_ge_ec, model = 'BRR'))

Multi-environment GS model

model1 = BGLR(y = dat$blue, ETA = ETA1_kernel, nIter = 12000, burnIn = 2000, 
              thin = 10, verbose = TRUE, saveAt = "Saves/dat/complete_met_ec_kernel")
model2 = BGLR(y = dat$blue, ETA = ETA1_brr, nIter = 12000, burnIn = 2000, 
              thin = 10, verbose = TRUE, saveAt = "Saves/dat/complete_met_ec_brr")
model3 = BGLR(y = dat$blue, ETA = ETA2_kernel, nIter = 12000, burnIn = 2000, 
              thin = 10, verbose = TRUE, saveAt = "Saves/dat/complete_met_geiec_kernel")
model4 = BGLR(y = dat$blue, ETA = ETA2_brr, nIter = 12000, burnIn = 2000, 
              thin = 10, verbose = TRUE, saveAt = "Saves/dat/complete_met_geiec_brr")

Multi-environment GS model

Ellapsed time

Show code
ellapsed = data.frame(
  model = rep(c("GxEC", "GxEC + GxE"), each = 2),
  mechanism = rep(c("RKHS", "CHOL"), times = 2),
  ellapsed = c(model1$ellapsed, model2$ellapsed, model3$ellapsed, model4$ellapsed)
)

ggplot(data = ellapsed, aes(x = model, y = ellapsed, fill = mechanism)) +
  geom_col(position = position_dodge(), color = "black") + 
  theme_bw() + 
  theme(text = element_text(size = 18), legend.position = 'top', 
        legend.title = element_blank()) +
  scale_fill_manual(values = c('#b2bc63','#10342d')) + 
  labs(x = "Model", y = "Ellapsed time (min)")

Multi-environment GS model

Model parameters

modparam = data.frame(
  residual = c(model1$varE, model2$varE, model3$varE, model4$varE),
  ec = c(model1$ETA$ec$varU, model2$ETA$ec$varB, model3$ETA$ec$varU, model4$ETA$ec$varB),
  g = c(model1$ETA$hyb$varU, model2$ETA$hyb$varB,  model3$ETA$hyb$varU, model4$ETA$hyb$varB),
  gxec = c(model1$ETA$gei_ec$varU,model2$ETA$gei_ec$varB, model3$ETA$gei_ec$varU, model4$ETA$gei_ec$varB),
  gxe = c(NA, NA, model3$ETA$gei$varU, model4$ETA$gei$varB),
  model = rep(c("GxEC", "GxEC + GxE"), each = 2),
  mechanism = rep(c("RKHS", "CHOL"), times = 2)
)
modparam
  residual       ec        g     gxec      gxe      model mechanism
1 2.213843 7.935072 1.739174 1.810245       NA       GxEC      RKHS
2 2.192019 8.904645 1.947036 1.855380       NA       GxEC      CHOL
3 1.859933 7.890657 1.743004 1.111975 1.169445 GxEC + GxE      RKHS
4 1.850351 8.759556 1.905014 1.169101 1.142758 GxEC + GxE      CHOL

Multi-environment GS model

Model parameters

Show code
modparam |> mutate(control = paste(model, mechanism, sep = "_")) %>% 
  group_by(control) |> 
  mutate(varT = ifelse(grepl("GxE_", control),residual + ec + g + gxec + gxe,
                       residual + ec + g + gxec),
         PvR = residual/varT,
         PvEC = ec/varT,
         PvG = g/varT,
         PvGxEC = gxec/varT,
         PvGxE = gxe/varT) |> 
  pivot_longer(PvR:PvGxE) |> 
  ggplot(aes(x = model, y = value)) +
  facet_wrap(.~mechanism) +
  geom_col(aes(fill = name), color = "black") + 
  theme_bw() + 
  labs(x = "Model", y = "Percentage from total variance") +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_viridis_d(option="mako", 
                       labels = c('PvR' = "varE", 'PvG' = "varG", 'PvGxE' = "varGEI",
                                  "PvGxEC" = "varE", "PvEC" = "varEC")) +
  theme(text = element_text(size = 18), legend.position = "top", 
        legend.title = element_blank())

Multi-environment GS model

Predictions

res = data.frame(gen = dat$gen, env = dat$env, blue = dat$blue,
                 'GxEC_RKHS' = model1$yHat, 'GxEC_CHOL' = model2$yHat,
                 'GxE_GxEC_RKHS' = model3$yHat, 'GxE_GxEC_CHOL' = model4$yHat)
Show code
GGally::ggpairs(res[,-c(1:3)]) + theme_bw() + 
  theme(text = element_text(size = 15),
        strip.text.y.right = element_text(size = 10))

Cross-validations

CV0: prediction of untested environments

Cross-validations

CV0 is carried out via leave-one-out cross-validation. That means that we cannot make multiple CV rounds with different training population compositions. The training population will always be the number of environments minus one.

cv0=list()
for (i in levels(dat$env)) {
  yNA = dat$blue
  yNA[dat$env == i] = NA
  
  mod1_cv = BGLR(y = yNA, ETA = ETA1_brr, nIter = 8000, burnIn = 1000, 
                thin = 10, verbose = FALSE, saveAt = "modCV")
  unlink(list.files(pattern = "modCV"))
  mod2_cv = BGLR(y = yNA, ETA = ETA2_brr, nIter = 8000, burnIn = 1000, 
                 thin = 10, verbose = FALSE, saveAt = "modCV")
  unlink(list.files(pattern = "modCV"))
  
  cv0[[i]] = data.frame(
    gen = dat$gen, env = dat$env, blue = dat$blue, yNA = yNA,
    yhat_GxEC = mod1_cv$yHat, yhat_GxE_GxEC = mod2_cv$yHat
  ) |> filter(env == i)
}

Cross-validations

CV0 results:

res_cv0 = lapply(cv0, function(x){
  pa_GxEC = cor(x$blue, x$yhat_GxEC, use = "complete.obs")
  pa_GxE_GxEC = cor(x$blue, x$yhat_GxE_GxEC, use = "complete.obs")
  rmspe_GxEC = sqrt(mean((x$blue - x$yhat_GxEC)^2, na.rm = T))
  rmspe_GxE_GxEC = sqrt(mean((x$blue - x$yhat_GxE_GxEC)^2, na.rm = T))
  data.frame(
    env = unique(x$env), 
    value = c(pa_GxEC, rmspe_GxEC, pa_GxE_GxEC, rmspe_GxE_GxEC),
    metric = c("pa", "rmspe"),
    model = rep(c("GxEC", "GxE_GxEC"), each = 2)
  )
})
res_cv0 = do.call(rbind, res_cv0)
tapply(res_cv0$value, res_cv0[,c("model", "metric")], mean)
          metric
model             pa    rmspe
  GxE_GxEC 0.4993338 2.688657
  GxEC     0.4820498 2.684478
res_cv0_mean = reframe(res_cv0, value = mean(value), .by = c(metric, model)) |> 
  mutate(CV = "CV0")

Cross-validations

CV00: prediction of untested genotypes in untested environments

Cross-validations

CV00 will mix CV0 and CV1:

nfolds = 5
nrept = 10
seed = 8  

sets = list()
i = 1
repeat{
  set.seed(7 * i)
  sets[[i]] = sample(rep(1:nfolds, length.out = nlevels(dat$gen)))
  i = i + 1
  if(i > nrept) break
}
cvdata = lapply(sets, function(x){
  cvdata = dat
  aux = data.frame(gen = levels(dat$gen), set = x)
  cvdata$set = aux$set[match(cvdata$gen, aux$gen)]
  return(cvdata)
})
for (i in 1:length(cvdata)) cvdata[[i]]$rept = i

Cross-validations

cv00 = lapply(cvdata, function(x){
  res.env = list()
  for (j in levels(x$env)) {
    res.list = list()
    for (i in unique(x$set)) {
      yNA = x$blue
      yNA[x$env == j | x$set == i] = NA
      
      mod1_cv = BGLR(y = yNA, ETA = ETA1_brr, nIter = 8000, burnIn = 1000, 
                     thin = 10, verbose = FALSE, saveAt = "modCV")
      unlink(list.files(pattern = "modCV"))
      mod2_cv = BGLR(y = yNA, ETA = ETA2_brr, nIter = 8000, burnIn = 1000, 
                     thin = 10, verbose = FALSE, saveAt = "modCV")
      unlink(list.files(pattern = "modCV"))
      
      res.list[[i]] = data.frame(
        gen = x$gen, env = x$env, set = x$set, blue = x$blue,
        yNA = yNA, yhat_GxEC = mod1_cv$yHat, yhat_GxE_GxEC = mod2_cv$yHat
      ) |> filter(env == j & set == i)
    }
    res.env[[j]] = do.call(rbind, res.list)
  }
  res.env
})

This will take a very long time to run: 5 folds \(\times\) 10 repetitions \(\times\) 12 environments \(\times\) 2 modelling strategies = 1200 models

Cross-validations

CV00 results:

res_cv00 = lapply(cv00, function(x){
  pa_GxEC = do.call(rbind, lapply(x, function(x) cor(x$blue, x$yhat_GxEC, use = "complete.obs")))
  rmspe_GxEC = do.call(rbind, lapply(x, function(x) sqrt(mean((x$blue - x$yhat_GxEC)^2, na.rm = T))))
  pa_GxE_GxEC = do.call(rbind, lapply(x, function(x) cor(x$blue, x$yhat_GxE_GxEC, use = "complete.obs")))
  rmspe_GxE_GxEC = do.call(rbind, lapply(x, function(x) sqrt(mean((x$blue - x$yhat_GxE_GxEC)^2, na.rm = T))))
  data.frame(
    env = rep(rownames(rmspe_GxEC), times = 4), 
    value = c(pa_GxEC, rmspe_GxEC, pa_GxE_GxEC, rmspe_GxE_GxEC),
    metric = rep(c("pa", "rmspe"), each = length(rownames(rmspe_GxEC))),
    model = rep(c("GxEC", "GxE_GxEC"), each = length(rownames(rmspe_GxEC))*2)
  )
})
res_cv00_mean = do.call(rbind, lapply(res_cv00, function(x) reframe(x, value = mean(value), .by = c(metric, model))))
res_cv00_mean$rept = rep(1:5, each = 4)
res_cv00_mean$CV = "CV00"

Cross-validations

CV2: Sparse-testing

sets = list()
i = 1
repeat{
  set.seed(987 * i)
  sets[[i]] = sample(rep(1:nfolds, length.out = nrow(dat)))
  i = i + 1
  if(i > nrept) break
}
cvdata = lapply(sets, function(x){
  cvdata = dat
  cvdata$set = x
  return(cvdata)
})
for (i in 1:length(cvdata)) cvdata[[i]]$rept = i

cv2 = lapply(cvdata, function(x){
  res.list = list()
  for (i in unique(x$set)) {
    
    yNA = x$blue
    yNA[x$set == i] = NA
    
    mod1_cv = BGLR(y = yNA, ETA = ETA1_brr, nIter = 8000, burnIn = 1000, 
                   thin = 10, verbose = FALSE, saveAt = "modCV")
    unlink(list.files(pattern = "modCV"))
    mod2_cv = BGLR(y = yNA, ETA = ETA2_brr, nIter = 8000, burnIn = 1000, 
                   thin = 10, verbose = FALSE, saveAt = "modCV")
    unlink(list.files(pattern = "modCV"))
    
    res.list[[i]] = data.frame(
      gen = x$gen,
      env = x$env,
      blue = x$blue,
      set = x$set,
      yNA = yNA,
      yhat_GxEC = mod1_cv$yHat,
      yhat_GxE_GxEC = mod2_cv$yHat
    ) |> filter(set == i)
    
  }
  res.list
})
cv2 = lapply(cv2, function(x) do.call(rbind, x))

Cross-validations

CV2 results:

res_cv2 = lapply(cv2, function(x){
  x |> pivot_longer(yhat_GxEC:yhat_GxE_GxEC, names_to = "model",
                    values_to = "yhat") |>
    mutate(model = gsub("yhat_","",model)) |> 
    reframe(rmspe = sqrt(mean((blue - yhat)^2, na.rm = T)),
            pa = cor(blue, yhat, use = "complete.obs"),
            .by = c(env, model)) |> 
    pivot_longer(rmspe:pa, names_to = "metric", values_to = "value") |> 
    as.data.frame()
})
res_cv2_mean = do.call(rbind, lapply(res_cv2, function(x) reframe(x, value = mean(value), .by = c(metric, model))))
res_cv2_mean$rept = rep(1:5, each = 4)
res_cv2_mean$CV = "CV2"

Cross-validations

CV1: Prediction of untested genotypes

sets = list()
i = 1
repeat{
  set.seed(7 * i)
  sets[[i]] = sample(rep(1:nfolds, length.out = nlevels(dat$gen)))
  i = i + 1
  if(i > nrept) break
}
cvdata = lapply(sets, function(x){
  cvdata = dat
  aux = data.frame(gen = levels(dat$gen), set = x)
  cvdata$set = aux$set[match(cvdata$gen, aux$gen)]
  return(cvdata)
})
for (i in 1:length(cvdata)) cvdata[[i]]$rept = i

cv1 = lapply(cvdata, function(x){
  res.list = list()
  for (i in unique(x$set)) {
    
    yNA = x$blue
    yNA[x$set == i] = NA
    
    mod1_cv = BGLR(y = yNA, ETA = ETA1_brr, nIter = 8000, burnIn = 1000, 
                   thin = 10, verbose = FALSE, saveAt = "modCV")
    unlink(list.files(pattern = "modCV"))
    mod2_cv = BGLR(y = yNA, ETA = ETA2_brr, nIter = 8000, burnIn = 1000, 
                   thin = 10, verbose = FALSE, saveAt = "modCV")
    unlink(list.files(pattern = "modCV"))
    
    res.list[[i]] = data.frame(
      gen = x$gen,
      env = x$env,
      blue = x$blue,
      set = x$set,
      yNA = yNA,
      yhat_GxEC = mod1_cv$yHat,
      yhat_GxE_GxEC = mod2_cv$yHat
    ) |> filter(set == i)
    
  }
  res.list
})
cv1 = lapply(cv1, function(x) do.call(rbind, x))

Cross-validations

CV1 results:

res_cv1 = lapply(cv1, function(x){
  x |> pivot_longer(yhat_GxEC:yhat_GxE_GxEC, names_to = "model",
                    values_to = "yhat") |>
    mutate(model = gsub("yhat_","",model)) |> 
    reframe(rmspe = sqrt(mean((blue - yhat)^2, na.rm = T)),
            pa = cor(blue, yhat, use = "complete.obs"),
            .by = c(env, model)) |> 
    pivot_longer(rmspe:pa, names_to = "metric", values_to = "value") |> 
    as.data.frame()
})
res_cv1_mean = do.call(rbind, lapply(res_cv1, function(x) reframe(x, value = mean(value), .by = c(metric, model))))
res_cv1_mean$rept = rep(1:5, each = 4)
res_cv1_mean$CV = "CV1"

Cross-validations

Show code
rbind(res_cv1_mean, res_cv2_mean, res_cv00_mean) |> 
  ggplot(aes(x = CV, y = value, fill = model)) +
  facet_wrap(.~metric, scales = "free_y", labeller = labeller(.cols = c("pa" = "Predictive ability", "rmspe" = "RMSPE"))) +
  geom_boxplot() +
  theme_bw() + 
  theme(axis.title.y = element_blank(), text = element_text(size = 18),
        legend.position = 'top') +
  labs(x = "Cross-validation scenario", fill = "Model") +
  scale_fill_manual("Model", values = c("#b2bc63", "#10342d"),
                    labels = c("GxE + GxEC", "GxE")) +
  scale_color_manual(values = c("#b2bc63", "#10342d"),
                    labels = c("GxE + GxEC", "GxE"), guide = 'none') +
  geom_linerange(
    data = res_cv0_mean, aes(x = CV, ymin = 0, ymax = value, color = model),
    position = position_dodge(width = 0.6),
    show.legend = FALSE,
    linewidth = 1
  ) +
  geom_point(
    data = res_cv0_mean, aes(x = CV, y = value, color = model),
    position = position_dodge(width = 0.6), 
    size = 2.5, 
    show.legend = FALSE, 
    shape = 17
  )

Cross-validations

Show code
for (i in 1:length(res_cv1)){
  res_cv1[[i]]$rept = i
  res_cv1[[i]]$CV = "CV1"
} 
for (i in 1:length(res_cv2)){
  res_cv2[[i]]$rept = i
  res_cv2[[i]]$CV = "CV2"
} 
for (i in 1:length(res_cv00)){
  res_cv00[[i]]$rept = i
  res_cv00[[i]]$CV = "CV00"
} 

rbind(
  do.call(rbind, res_cv1),
  do.call(rbind, res_cv2),
  do.call(rbind, res_cv00)
) |>
  ggplot(aes(x = env, y = value, fill = model)) +
  facet_grid(
    metric ~ CV, 
    scales = 'free_y',
    labeller = labeller(.rows = c("pa" = "Predictive ability", "rmspe" = "RMSPE"))
  ) +
  theme_bw() + 
  theme(
    axis.title.y = element_blank(), 
    text = element_text(size = 14),
    axis.text.x = element_text(angle = 90, vjust = .5, hjust = 1),
    legend.position = "top"
  ) +
  geom_boxplot() +
  labs(x = "Environments", fill = "Model") +
  scale_fill_manual(
    values = c("#b2bc63", "#10342d"),
    labels = c("GxE + GxEC", "GxE")
  ) +
  scale_color_manual(
    values = c("#b2bc63", "#10342d"),
    labels = c("GxE + GxEC", "GxE"), 
    guide = 'none'
  ) +
  geom_linerange(
    data = res_cv0 |> mutate(CV = "CV0"),
    aes(x = env, ymin = 0, ymax = value, color = model, group = model),
    position = position_dodge(width = 0.6),
    show.legend = FALSE,
    linewidth = 1, alpha = .7
  ) +
  geom_point(
    data = res_cv0 |> mutate(CV = "CV0"), 
    aes(x = env, y = value, color = model, group = model),
    position = position_dodge(width = 0.6), 
    size = 2.5, 
    show.legend = FALSE, 
    shape = 17, 
    alpha = .7
  )

References

Couto, E. G. O., Chaves, S. F. S., Dias, K. O. G., Morales-Marroqu, J. A., Alves-Pereira, A., Colombo, C. A., & Zucchi, M. I. (2024). Training set optimization is a feasible alternative for perennial orphan crop domestication and germplasm management: an Acrocomia aculeata example. Frontiers in Plant Science, 15, 1441683. https://doi.org/10.3389/fpls.2024.1441683
de los Campos, G., Hickey, J. M., Pong-Wong, R., Daetwyler, H. D., & Calus, M. P. L. (2013). Whole-genome regression and prediction methods applied to plant and animal breeding. Genetics, 193(2), 327–345. https://doi.org/10.1534/genetics.112.143313
Fernández-González, J., Akdemir, D., & Isidro y Sánchez, J. (2023). A comparison of methods for training population optimization in genomic selection. Theoretical and Applied Genetics, 136(3), 30. https://doi.org/10.1007/s00122-023-04265-6
Fritsche-Neto, R., Resende, R. T., Olivoto, T., Garcia-Abadillo, J., Nascimento, M., Bahia, M. A. M., Jarquin, D., & Vieira, R. A. (2025). Prediction-based breeding: Modern tools to optimize and reshape programs. Crop Science, 65(5), e70175. https://doi.org/10.1002/csc2.70175
Hickey, J. M., Chiurugwi, T., Mackay, I., & Powell, W. (2017). Genomic prediction unifies animal and plant breeding programs to form platforms for biological discovery. Nature Genetics, 49(9), 1297–1303. https://doi.org/10.1038/ng.3920
Jarquin, D., Crossa, J., Lacaze, X., Du Cheyron, P., Daucourt, J., Lorgeou, J., Piraux, F., Guerreiro, L., Pérez, P., Calus, M., Burgueño, J., & de los Campos, G. (2014). A reaction norm model for genomic selection using high-dimensional genomic and environmental data. Theoretical and applied genetics, 127(3), 595–607. https://doi.org/10.1007/s00122-013-2243-1
Jarquin, D., Howard, R., Crossa, J., Beyene, Y., Gowda, M., Martini, J. W. R., Covarrubias Pazaran, G., Burgueño, J., Pacheco, A., Grondona, M., Wimmer, V., & Prasanna, B. M. (2020). Genomic prediction enhanced sparse testing for multi-environment trials. G3 GenesGenomesGenetics, 10(8), 2725–2739. https://doi.org/10.1534/g3.120.401349
Marcatti, G. E., & Resende, R. T. (2026). The Enviromic marker. Crop Breeding and Applied Biotechnology, 26, e54222611. https://doi.org/https://doi.org/10.1590/1984-70332026v26n1n1
Misztal, I., Aguilar, I., Lourenco, D., Ma, L., Steibel, J. P., & Toro, M. (2021). Emerging issues in genomic selection. Journal of Animal Science, 99(6), skab092. https://doi.org/10.1093/jas/skab092
Pérez, P., & de los Campos, G. (2014). Genome-wide regression and prediction with the BGLR statistical package. Genetics, 198(2), 483–495. https://doi.org/10.1534/genetics.114.164442
Voss-Fels, K. P., Cooper, M., & Hayes, B. J. (2019). Accelerating crop genetic gains with genomic selection. Theoretical and Applied Genetics, 132(3), 669–686. https://doi.org/10.1007/s00122-018-3270-8
Wallace, J. G., Rodgers-Melnick, E., & Buckler, E. S. (2018). On the road to breeding 4.0: Unraveling the good, the bad, and the boring of crop quantitative genomics. Annual Review of Genetics, 52(1), 421–444. https://doi.org/10.1146/annurev-genet-120116-024846
Werner, C. R., Gaynor, R. C., Sargent, D. J., Lillo, A., Gorjanc, G., & Hickey, J. M. (2023). Genomic selection strategies for clonally propagated crops. Theoretical and Applied Genetics, 136(4), 74. https://doi.org/10.1007/s00122-023-04300-6