| Title: | Envirotyping for Quantitative Genetics and Plant Breeding |
|---|---|
| Description: | An enviromics toolkit for genotype-by-environment analysis and envirotype-informed genomic prediction. Collects worldwide daily and hourly weather from 'NASA POWER', soil profiles from 'SoilGrids', elevation and bioclimatic layers from 'WorldClim', agro-ecological zones from 'FAO GAEZ', and downscaled 'CMIP6' climate scenarios, with checkpointed retrieval that can resume after an interrupted run. Derives agro-meteorological parameters (solar radiation, evapotranspiration, thermal time, atmospheric demand), runs a FAO-56 single crop coefficient soil water balance, and maps daily weather onto crop developmental stages. Builds environmental covariable matrices, envirotype typologies and frequency matrices, mega-environment clusters, historical risk profiles and target population of environments weights. Reduces interval-resolved envirotypes by principal component analysis, delineates soil zones with Gaussian mixture models, and constructs linear, Gaussian and arc-cosine environmental relatedness kernels. Fits Bayesian reaction-norm kernel models and predicts trained genotypes at untested locations and planting dates, with coverage diagnostics, spatial mapping and planting-window optimisation. Simulation functions generate multi-environment trials and enviromes with known architecture for power analysis. Methods are described in Costa-Neto, Galli, Carvalho, Crossa and Fritsche-Neto (2021) <doi:10.1093/g3journal/jkab040>, and build on the Bayesian genotype-by-environment kernel sampler of Granato, Cuevas, Luna-Vazquez, Crossa, Montesinos-Lopez, Burgueno and Fritsche-Neto (2018) <doi:10.1534/g3.118.200435>. |
| Authors: | Germano Costa-Neto [aut, cre], Fernanda Pontes [aut] |
| Maintainer: | Germano Costa-Neto <[email protected]> |
| License: | GPL-3 |
| Version: | 0.1.3 |
| Built: | 2026-10-22 19:27:17 UTC |
| Source: | https://github.com/cran/EnvRtype |
Returns the URL template get_AEZ uses when no local file is given
and the bundled raster is unavailable.
aez_default_url(layer = "aez57")aez_default_url(layer = "aez57")
layer |
character. Which GAEZ layer. Currently |
This URL is unverified. It was constructed from the documented GAEZ v4
portal structure but could not be tested against a live FAO server, and FAO has
reorganised these paths between releases. It is exposed as a function, rather
than hidden inside get_AEZ(), so it can be inspected and overridden
without editing the package:
get_AEZ(env, lat, lon, url = "https://.../correct/path.tif")
For reproducible analyses, rely on the bundled raster or download the layer
once and pass file=.
A single character string.
aez_default_url()aez_default_url()
Returns the class table used to decode GAEZ agro-ecological zone raster values into human-readable zone names.
aez_legend(classes = 57)aez_legend(classes = 57)
classes |
integer. Which legend to return. Currently only |
Verify before relying on these labels. FAO has revised AEZ class
definitions between GAEZ releases. The table here follows the GAEZ v4
documentation, but if your raster came from a different release the codes may
not line up. get_AEZ will warn when a raster contains codes that
are absent from the legend – that warning is the signal to check.
A data.frame with code, label, thermal, moisture
and arable (logical; FALSE for water, urban, barren and the
constraint classes).
leg <- aez_legend() head(leg) table(leg$thermal)leg <- aez_legend() head(leg) table(leg$thermal)
Returns the long phenotype table, so a sim_met object can
be handed straight to modelling functions or inspected as a plain frame.
## S3 method for class 'sim_met' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'sim_met' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
a |
row.names, optional
|
passed to the default method for consistency; unused. |
... |
ignored. |
A data.frame with one row per plot and columns env,
gid, rep and value.
Coerce a Simulated Envirome Diversity Matrix to a Plain Matrix
## S3 method for class 'sim_envirome' as.matrix(x, ...)## S3 method for class 'sim_envirome' as.matrix(x, ...)
x |
a |
... |
ignored. |
A numeric matrix with no sim_envirome class or attributes.
Strips the sim_W class and its diagnostic attributes,
returning the bare covariable matrix for use with
env_kernel, W_matrix consumers, or base matrix ops.
## S3 method for class 'sim_W' as.matrix(x, ...)## S3 method for class 'sim_W' as.matrix(x, ...)
x |
a |
... |
ignored. |
A numeric matrix (environments in rows, covariables in columns) with
no sim_W class or attributes.
Best planting date per genotype
best_planting_date( scan, dates = NULL, date_col = "date", genotypes = NULL, group = NULL, exclude_flagged = TRUE, envelope_level = c("gross", "mild") )best_planting_date( scan, dates = NULL, date_col = "date", genotypes = NULL, group = NULL, exclude_flagged = TRUE, envelope_level = c("gross", "mild") )
scan, dates, date_col, genotypes, group
|
as in [planting_window_table()]. |
exclude_flagged |
drop dates outside the envelope before ranking. Default TRUE: an optimum found in a region the training data does not cover is not an optimum you can act on. |
envelope_level |
'"gross"' (default) or '"mild"'. |
data.frame: 'gid', 'best_date', 'yHat', 'lower', 'upper', 'position', 'n_dates_considered'.
## A scan is normally produced by scan_untested_envs(); here we build a ## minimal one by hand -- 3 genotypes across 6 candidate sowing windows -- ## so the example is self-contained. gids <- paste0("G", 1:3) envs <- paste0("sow", 1:6) mu <- matrix(c(6, 5.5, 5), 3, 6) + matrix(rep(c(0, .4, .7, .6, .2, -.3), each = 3), 3, 6) # peaks mid-window dimnames(mu) <- list(gids, envs) scan <- list( yHat_matrix = mu, yHat_lower = mu - 0.5, yHat_upper = mu + 0.5, interpolation = data.frame( env = envs, position = c("interpolation", "interpolation", "edge", "interpolation", "mild_extrapolation", "gross_extrapolation"), mahalanobis_ratio = c(0.6, 0.8, 1.4, 0.9, 2.1, 3.6), prop_outside = c(0, 0, 0.1, 0, 0.3, 0.7), weight_negativity = 0, stringsAsFactors = FALSE)) class(scan) <- "scan_untested_envs" ## Map each scanned environment to a calendar sowing date dates <- data.frame(env = envs, date = as.Date("2024-04-01") + c(0, 14, 28, 42, 56, 70)) ## Best *supported* sowing date per genotype (gross extrapolations dropped) best_planting_date(scan, dates) ## Rank every date, including the out-of-envelope ones best_planting_date(scan, dates, exclude_flagged = FALSE) ## Stricter: also drop mild extrapolations before ranking best_planting_date(scan, dates, envelope_level = "mild")## A scan is normally produced by scan_untested_envs(); here we build a ## minimal one by hand -- 3 genotypes across 6 candidate sowing windows -- ## so the example is self-contained. gids <- paste0("G", 1:3) envs <- paste0("sow", 1:6) mu <- matrix(c(6, 5.5, 5), 3, 6) + matrix(rep(c(0, .4, .7, .6, .2, -.3), each = 3), 3, 6) # peaks mid-window dimnames(mu) <- list(gids, envs) scan <- list( yHat_matrix = mu, yHat_lower = mu - 0.5, yHat_upper = mu + 0.5, interpolation = data.frame( env = envs, position = c("interpolation", "interpolation", "edge", "interpolation", "mild_extrapolation", "gross_extrapolation"), mahalanobis_ratio = c(0.6, 0.8, 1.4, 0.9, 2.1, 3.6), prop_outside = c(0, 0, 0.1, 0, 0.3, 0.7), weight_negativity = 0, stringsAsFactors = FALSE)) class(scan) <- "scan_untested_envs" ## Map each scanned environment to a calendar sowing date dates <- data.frame(env = envs, date = as.Date("2024-04-01") + c(0, 14, 28, 42, 56, 70)) ## Best *supported* sowing date per genotype (gross extrapolations dropped) best_planting_date(scan, dates) ## Rank every date, including the out-of-envelope ones best_planting_date(scan, dates, exclude_flagged = FALSE) ## Stricter: also drop mild extrapolations before ranking best_planting_date(scan, dates, envelope_level = "mild")
Soil profile for clay content (g/kg) around Nairobi, downloaded from SoilGrids
(https://soilgrids.org/). Shipped as a GeoTIFF under inst/extdata and read
with terra.
A single-layer GeoTIFF raster read with terra::rast().
f <- system.file("extdata", "clay_5_15.tif", package = "EnvRtype") clay <- terra::rast(f) terra::plot(clay)f <- system.file("extdata", "clay_5_15.tif", package = "EnvRtype") clay <- terra::rast(f) terra::plot(clay)
Delineates mega-environments by clustering the genetic correlation among environments, i.e. how similarly genotypes rank from one environment to another. Environments in which genotypes respond alike are placed in the same cluster, which is the operational definition of a mega-environment in a target population of environments (TPE).
The input is either a fitted kernel_model (carrying the
environment genetic-correlation matrix in $genetic_cor_env, produced
with keep_effects = TRUE) or any square environment correlation matrix
you already have (for example from env_cor or an external GxE
analysis).
cluster_environments( object, k = NULL, method = c("hclust", "pam", "kmeans"), distance = c("one_minus", "sqrt"), hclust_linkage = "ward.D2", k_range = 2:6, seed = NULL )cluster_environments( object, k = NULL, method = c("hclust", "pam", "kmeans"), distance = c("one_minus", "sqrt"), hclust_linkage = "ward.D2", k_range = 2:6, seed = NULL )
object |
a |
k |
integer or NULL. Number of clusters. When |
method |
character. Clustering algorithm: |
distance |
character. Distance built from the genetic correlation:
|
hclust_linkage |
character. Linkage passed to |
k_range |
integer vector. Candidate numbers of clusters tested when
|
seed |
integer or NULL. Optional seed for reproducibility of the
randomised starts in |
The genetic correlation is turned into a distance and the chosen
algorithm is run on that distance:
distance = "one_minus" uses (default).
distance = "sqrt" uses , which spreads
out highly correlated environments.
Correlations are clamped to and the distance matrix is
symmetrised before clustering. Three algorithms are available through
method: agglomerative hierarchical clustering ("hclust",
controlled by hclust_linkage), partitioning around medoids
("pam"), and "kmeans" run on a classical MDS embedding of the
distance matrix.
When k is NULL the number of clusters is selected automatically
by maximising the average silhouette width over the candidate values
in k_range; supplying k skips this search. At least three
environments are required.
A list with:
named integer vector giving the cluster of each environment.
the number of clusters used (selected or supplied).
the clustering algorithm used.
the symmetric distance matrix that was clustered.
per-environment silhouette widths plus their
average (higher is a tighter, better-separated solution).
representative environment of each cluster
("pam"/"kmeans"; NULL for "hclust").
the hclust tree for method = "hclust",
otherwise NULL.
a data.frame (env, cluster, sil_width)
ordered by cluster and decreasing silhouette width.
kernel_model (fit with keep_effects = TRUE to
obtain $genetic_cor_env), kernel_model_clustered to fit
a model informed by the clusters, env_cluster for clustering
from an environmental covariable matrix, and env_cor.
## ------------------------------------------------------------------ ## 1. Cluster a genetic-correlation matrix directly (no model needed) ## ------------------------------------------------------------------ ## Six environments forming two obvious groups R <- matrix(0.2, 6, 6) R[1:3, 1:3] <- 0.9 R[4:6, 4:6] <- 0.85 diag(R) <- 1 dimnames(R) <- list(paste0("E", 1:6), paste0("E", 1:6)) ## Automatic number of clusters via silhouette width cl <- cluster_environments(R) cl$k cl$table cl$silhouette["average"] ## ------------------------------------------------------------------ ## 2. Force a specific number of clusters ## ------------------------------------------------------------------ cluster_environments(R, k = 2)$clusters ## ------------------------------------------------------------------ ## 3. Try different algorithms and distances ## ------------------------------------------------------------------ cluster_environments(R, k = 2, method = "pam", seed = 1)$medoids cluster_environments(R, method = "kmeans", k_range = 2:4, seed = 1)$clusters cluster_environments(R, distance = "sqrt", hclust_linkage = "complete")$clusters ## ------------------------------------------------------------------ ## 4. Full workflow from phenotypes, kernels and a fitted model ## ------------------------------------------------------------------ data("maizeYield"); data("maizeG"); data("maizeWTH") ECs <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("FRUE", "PETP", "SRAD", "T2M_MAX")) K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = ECs)[[2]]), data = maizeYield, model = "RNMM") ## keep_effects = TRUE is required for $genetic_cor_env fit <- kernel_model(y = "value", env = "env", gid = "gid", data = maizeYield, random = K, keep_effects = TRUE) megaenv <- cluster_environments(fit) megaenv$table ## 5. Feed the clusters into a cluster-aware model fit_cl <- kernel_model_clustered(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", clusters = megaenv, use = "block")## ------------------------------------------------------------------ ## 1. Cluster a genetic-correlation matrix directly (no model needed) ## ------------------------------------------------------------------ ## Six environments forming two obvious groups R <- matrix(0.2, 6, 6) R[1:3, 1:3] <- 0.9 R[4:6, 4:6] <- 0.85 diag(R) <- 1 dimnames(R) <- list(paste0("E", 1:6), paste0("E", 1:6)) ## Automatic number of clusters via silhouette width cl <- cluster_environments(R) cl$k cl$table cl$silhouette["average"] ## ------------------------------------------------------------------ ## 2. Force a specific number of clusters ## ------------------------------------------------------------------ cluster_environments(R, k = 2)$clusters ## ------------------------------------------------------------------ ## 3. Try different algorithms and distances ## ------------------------------------------------------------------ cluster_environments(R, k = 2, method = "pam", seed = 1)$medoids cluster_environments(R, method = "kmeans", k_range = 2:4, seed = 1)$clusters cluster_environments(R, distance = "sqrt", hclust_linkage = "complete")$clusters ## ------------------------------------------------------------------ ## 4. Full workflow from phenotypes, kernels and a fitted model ## ------------------------------------------------------------------ data("maizeYield"); data("maizeG"); data("maizeWTH") ECs <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("FRUE", "PETP", "SRAD", "T2M_MAX")) K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = ECs)[[2]]), data = maizeYield, model = "RNMM") ## keep_effects = TRUE is required for $genetic_cor_env fit <- kernel_model(y = "value", env = "env", gid = "gid", data = maizeYield, random = K, keep_effects = TRUE) megaenv <- cluster_environments(fit) megaenv$table ## 5. Feed the clusters into a cluster-aware model fit_cl <- kernel_model_clustered(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", clusters = megaenv, use = "block")
Tabulates how much of a scanned area falls in each envelope class, and which covariates most often drive sites outside. The second table answers "what should I go and measure".
coverage_summary(scan)coverage_summary(scan)
scan |
a 'scan_untested_envs' / [grid_scan()] result. |
list of 'by_position' and 'by_covariate' data.frames.
Turns each dense kernel (list(Kernel = <n x n>, Type = "D"|"BD")) into
a cached spectral decomposition stored on $decomp. Call once and reuse
across any number of kernel_model fits: kernels depend only on
genotypes and environments – never on the phenotype vector – so masking
y for a cross-validation fold does NOT invalidate the decomposition.
decompose_kernels( K, ne = NULL, tol = 1e-10, scale_kernels = TRUE, keep_var = 1, verbose = TRUE )decompose_kernels( K, ne = NULL, tol = 1e-10, scale_kernels = TRUE, keep_var = 1, verbose = TRUE )
K |
list of kernels. |
ne |
per-environment observation counts; defaults to |
tol |
eigenvalue cutoff; must match |
scale_kernels |
must match |
keep_var |
numeric in (0, 1]. Retain only the leading eigenvalues
carrying this proportion of each kernel's trace. 1 (default) keeps
everything. A named vector or list keyed by kernel name / regex applies
per kernel, e.g. |
verbose |
report rank, timing and route per kernel. |
Factored kernels from get_kernel arrive pre-decomposed and do
not need this function.
K with $decomp on each element and a "kd_meta" attribute.
get_kernel, kernel_model, undecompose_kernels
Clusters environments into homogeneous groups ("mega-environments") from an environmental
covariable matrix (W) using K-means. The number of groups can be set by the user or
selected automatically by maximising the average silhouette width. An environmental
relationship (kinship) matrix is also returned for convenience.
env_cluster( W, k = NULL, k.max = 10, scale = TRUE, nstart = 25, seed = NULL, verbose = TRUE )env_cluster( W, k = NULL, k.max = 10, scale = TRUE, nstart = 25, seed = NULL, verbose = TRUE )
W |
matrix or data.frame. Environmental covariables (environments in rows, variables in
columns), typically a |
k |
integer or NULL. Number of mega-environments. If |
k.max |
integer. Maximum number of clusters tested when |
scale |
boolean. If |
nstart |
integer. Number of random starts for K-means. Default 25. |
seed |
integer or NULL. Optional seed for reproducibility. |
verbose |
boolean. If |
Mega-environments are repeatable groups of locations that rank genotypes similarly. Here they
are approximated by grouping environments that share a similar enviromic profile, which is
useful to stratify a Target Population of Environments (TPE) before multi-environment
prediction. The silhouette width contrasts within- and between-cluster distances;
the k maximising its average is retained.
A list with clusters (named integer vector), k, silhouette (data.frame
of average silhouette per tested k, or NULL when k was supplied),
kmeans (the fitted object) and env.kinship (an environment x environment
correlation matrix).
Germano Costa Neto
W_matrix, env_kernel, env_target_importance,
T_matrix
data("maizeWTH") W <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## Automatic number of mega-environments (chosen by silhouette) mega <- env_cluster(W, seed = 1) mega$k mega$clusters mega$silhouette ## Force a fixed number of groups mega2 <- env_cluster(W, k = 2, seed = 1) mega2$clusters ## Environmental relationship (kinship) matrix round(mega$env.kinship, 2) ## Use the grouping downstream (e.g. as a fixed effect or to stratify CV) table(mega$clusters)data("maizeWTH") W <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## Automatic number of mega-environments (chosen by silhouette) mega <- env_cluster(W, seed = 1) mega$k mega$clusters mega$silhouette ## Force a fixed number of groups mega2 <- env_cluster(W, k = 2, seed = 1) mega2$clusters ## Environmental relationship (kinship) matrix round(mega$env.kinship, 2) ## Use the grouping downstream (e.g. as a fixed effect or to stratify CV) table(mega$clusters)
Performs a copula analysis of an environment x covariable matrix (W) and returns a
matrix of copula indices that can replace the raw covariables. Sklar's theorem lets
the joint distribution of the environmental variables be split into (i) the marginal
behaviour of each variable and (ii) the dependence structure linking them (the copula).
By moving to the copula scale, each environment is described by where it sits in the
multivariate environmental distribution rather than by raw physical units.
env_copula( W, index = c("pobs", "joint", "survival", "kendall", "all"), groups = NULL, ties.method = "average", normalize = TRUE, verbose = TRUE )env_copula( W, index = c("pobs", "joint", "survival", "kendall", "all"), groups = NULL, ties.method = "average", normalize = TRUE, verbose = TRUE )
W |
matrix or data.frame. Environmental covariables with environments in rows and
variables in columns (typically a |
index |
character. Which copula index to return:
|
groups |
list or NULL. Optional named list splitting the columns of |
ties.method |
character. How ranks are computed when there are ties, passed to
|
normalize |
boolean. If |
verbose |
boolean. If |
Why rank-based. All indices are computed from ranks, so they are invariant to any monotone transformation of the marginals: storing precipitation in mm or log(mm) gives the same result, and no distributional assumption is imposed. This is the main practical gain over a z-scored W, where one extreme site can dominate the Euclidean geometry.
Pseudo-observations. For environments and variable ,
with the rank of environment . Dividing by keeps values
strictly inside , avoiding boundary problems in downstream density evaluation.
Empirical copula. The joint index is the empirical copula evaluated at each
environment's own pseudo-observation, a nonparametric estimator of that requires no
copula family to be chosen.
Kendall function. is the CDF of , estimated by the
Genest-Rivest empirical construction. It collapses a -dimensional dependence
structure into one interpretable number per environment and underpins multivariate return
periods in hydrology.
Sample-size caveat. These are rank statistics across environments, so their
resolution is bounded by . With five environments each pseudo-observation can only
take values in 1/6,...,5/6 and the joint copula is very coarse; the indices become
informative with dozens of environments. A warning is issued when .
Scope. The dependence modelled is that among environmental variables across
the environment panel. Geographic coordinates are not used and no spatial autocorrelation
(variogram / distance decay) is fitted. Use groups to control which variables share
a copula.
A numeric matrix with environments in rows (rownames preserved from W) and
the requested copula indices in columns. Attributes "copula.index" and
"copula.groups" record the index produced and the block structure used.
Germano Costa Neto
Sklar, A. (1959). Fonctions de repartition a n dimensions et leurs marges. Publ. Inst. Statist. Univ. Paris 8, 229-231.
Genest, C., Rivest, L.-P. (1993). Statistical inference procedures for bivariate Archimedean copulas. JASA 88, 1034-1043.
Salvadori, G., De Michele, C., Kottegoda, N.T., Rosso, R. (2007). Extremes in Nature: An Approach Using Copulas. Springer.
W_matrix, env_kernel, env_cluster
data("maizeWTH") env.data <- maizeWTH[maizeWTH$daysFromStart < 100, ] W <- W_matrix(env.data = env.data, var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## 1. Marginal copula scale (pseudo-observations) round(env_copula(W, index = "pobs"), 3) ## 2. Joint non-exceedance probability per environment env_copula(W, index = "joint") ## 3. Compound-stress (joint exceedance) index env_copula(W, index = "survival") ## 4. Kendall multivariate return level env_copula(W, index = "kendall") ## 5. Blocks: heat and water handled as separate copulas env_copula(W, index = "all", groups = list(heat = c("T2M_mean", "T2M_MAX_mean"), water = "PRECTOT_mean")) ## 6. Straight from W_matrix() via the 'copula' argument Wc <- W_matrix(env.data = env.data, var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean", copula = "kendall", center = FALSE, scale = FALSE) ## 7. Copula-scale kernel for reaction-norm models Kc <- env_kernel(env.data = env_copula(W, index = "pobs"), gaussian = TRUE) round(Kc$envCov, 2)data("maizeWTH") env.data <- maizeWTH[maizeWTH$daysFromStart < 100, ] W <- W_matrix(env.data = env.data, var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## 1. Marginal copula scale (pseudo-observations) round(env_copula(W, index = "pobs"), 3) ## 2. Joint non-exceedance probability per environment env_copula(W, index = "joint") ## 3. Compound-stress (joint exceedance) index env_copula(W, index = "survival") ## 4. Kendall multivariate return level env_copula(W, index = "kendall") ## 5. Blocks: heat and water handled as separate copulas env_copula(W, index = "all", groups = list(heat = c("T2M_mean", "T2M_MAX_mean"), water = "PRECTOT_mean")) ## 6. Straight from W_matrix() via the 'copula' argument Wc <- W_matrix(env.data = env.data, var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean", copula = "kendall", center = FALSE, scale = FALSE) ## 7. Copula-scale kernel for reaction-norm models Kc <- env_kernel(env.data = env_copula(W, index = "pobs"), gaussian = TRUE) round(Kc$envCov, 2)
Accessory generic that pulls the environment correlation out of a
sim_met object or a bare matrix, so a hand-off to
sim_W never depends on internal structure.
env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'sim_met' env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'matrix' env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'sim_W' env_cor(x, type = c("target", "realised"), ...) ## Default S3 method: env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'sim_envirome' env_cor(x, type = c("target", "realised"), ...)env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'sim_met' env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'matrix' env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'sim_W' env_cor(x, type = c("target", "realised"), ...) ## Default S3 method: env_cor(x, type = c("target", "realised"), ...) ## S3 method for class 'sim_envirome' env_cor(x, type = c("target", "realised"), ...)
x |
an object ( |
type |
"target" (the specified correlation) or "realised" (what the simulation actually produced, on the K-whitened scale). |
... |
unused. |
A correlation matrix.
met <- sim_met(maizeG, n_env = 6, seed = 1) env_cor(met, "target") env_cor(met, "realised")met <- sim_met(maizeG, n_env = 6, seed = 1) env_cor(met, "target") env_cor(met, "realised")
Pearson correlation among all environmental indices, as in Fig. 1b of Della Coletta et al. (2023), which motivated the dimensionality reduction: the 867 indices were highly correlated.
env_cor_heatmap( W, order = c("factor", "interval", "none"), max.index = 400L, title = "Correlation among environmental indices", verbose = TRUE )env_cor_heatmap( W, order = c("factor", "interval", "none"), max.index = 400L, title = "Correlation among environmental indices", verbose = TRUE )
W |
matrix. Environment x index matrix from |
order |
character. Column ordering: |
max.index |
integer. Guard against enormous plots; if the matrix has more columns than this, a regularly spaced subset is shown. Default 400. |
title |
character. Plot title. |
verbose |
boolean. If |
The mean absolute off-diagonal correlation is printed as a one-number summary of how redundant the index set is – the quantity that justifies running a PCA at all.
Invisibly, the correlation matrix that was plotted.
W <- env_indices(maizeWTH, end.day = 130) R <- env_cor_heatmap(W)W <- env_indices(maizeWTH, end.day = 130) R <- env_cor_heatmap(W)
Expands an environment-level W matrix to the observations of a
phenotypic data.frame, so that each row of the output corresponds to one
genotype-in-environment record.
env_expand(env.data, df.pheno, env.id = "env", skip = 3, verbose = TRUE)env_expand(env.data, df.pheno, env.id = "env", skip = 3, verbose = TRUE)
env.data |
matrix. Environment x covariable matrix with environments as rownames. |
df.pheno |
data.frame. Phenotypic records containing the environment column. |
env.id |
character. Name of the environment column in |
skip |
integer. Number of leading columns of the merged table to drop (the phenotypic id columns). Default 3. |
verbose |
boolean. If |
A numeric matrix of rows (observations) by covariables.
W_matrix, env_kernel
data("maizeYield"); data("maizeWTH") W <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "PRECTOT"), statistic = "mean") Wn <- env_expand(env.data = W, df.pheno = maizeYield, env.id = "env") dim(Wn)data("maizeYield"); data("maizeWTH") W <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "PRECTOT"), statistic = "mean") Wn <- env_expand(env.data = W, df.pheno = maizeYield, env.id = "env") dim(Wn)
Converts daily environmental data into the environment x index matrix used by Della Coletta et al. (2023): each weather factor is summarised within fixed-width time intervals from planting, producing one column per factor x interval combination.
env_indices( env.data, env.id = "env", day.id = "daysFromStart", var.id = NULL, interval = 3L, end.day = 151L, statistic = c("mean", "sum", "min", "max", "median"), statistic.by = NULL, id.names = NULL, sep = "_", require.complete = TRUE, verbose = TRUE )env_indices( env.data, env.id = "env", day.id = "daysFromStart", var.id = NULL, interval = 3L, end.day = 151L, statistic = c("mean", "sum", "min", "max", "median"), statistic.by = NULL, id.names = NULL, sep = "_", require.complete = TRUE, verbose = TRUE )
env.data |
data.frame of daily weather, e.g. a |
env.id |
character. Column identifying the environment. Default |
day.id |
character. Column giving days from planting. Default |
var.id |
character vector. Which weather factors to use. |
interval |
integer. Width of each time window in days. Default 3, as in the paper. |
end.day |
integer. Last day retained. Default 151, as in the paper. Days beyond this are discarded so that every environment contributes the same intervals. |
statistic |
character. Within-interval summary: |
statistic.by |
named character vector. Per-factor override of |
id.names |
character vector. Additional columns to treat as identifiers
(never summarised). Default |
sep |
character. Separator between factor and interval in the output column
names. Default |
require.complete |
boolean. If |
verbose |
boolean. Print a summary. Default |
Why a fixed end day. Environments differ in season length. Without a common
cut-off, environments contribute different numbers of intervals and the matrix is
ragged. The paper fixed the last interval at 151 days after planting; the same device
is used here via end.day.
Interval labelling. With interval = 3 and end.day = 151, days
1-3 form interval 1, days 4-6 interval 2, and so on; the final partial window
(day 151) forms interval 51. This reproduces the 51 intervals of the paper, and with
17 factors gives the 867 indices reported.
Mean vs sum. The paper does not state the within-interval statistic.
A mean is used by default because it is defined for every factor, but for
accumulating quantities such as precipitation a sum is more interpretable;
statistic.by allows a per-factor choice.
A numeric matrix with environments in rows and
indices in columns. Attributes:
"factors"the factor names used
"intervals"the interval labels
"interval.mid"named numeric vector of interval mid-point days
"interval.range"data.frame of interval start/end days
"n.missing"number of missing cells
Implementation following Della Coletta et al. (2023)
Della Coletta R., Liese S.E., Fernandes S.B., Mikel M.A., Bohn M.O., Lipka A.E., Hirsch C.N. (2023). Linking genetic and environmental factors through marker effect networks to understand trait plasticity. GENETICS 224(4), iyad103.
data("maizeWTH") ## 3-day intervals across the shared season. The bundled sample does not ## reach the paper's 151 days in every environment, so end.day is trimmed to ## a fully observed window (the default 151 would leave gaps and error out). W <- env_indices(maizeWTH, env.id = "env", day.id = "daysFromStart", end.day = 130) dim(W) attr(W, "intervals")[1:5] ## Sum precipitation within each window, average everything else W2 <- env_indices(maizeWTH, statistic = "mean", statistic.by = c(PRECTOT = "sum"), end.day = 130) ## Weekly windows instead W3 <- env_indices(maizeWTH, interval = 7, end.day = 140)data("maizeWTH") ## 3-day intervals across the shared season. The bundled sample does not ## reach the paper's 151 days in every environment, so end.day is trimmed to ## a fully observed window (the default 151 would leave gaps and error out). W <- env_indices(maizeWTH, env.id = "env", day.id = "daysFromStart", end.day = 130) dim(W) attr(W, "intervals")[1:5] ## Sum precipitation within each window, average everything else W2 <- env_indices(maizeWTH, statistic = "mean", statistic.by = c(PRECTOT = "sum"), end.day = 130) ## Weekly windows instead W3 <- env_indices(maizeWTH, interval = 7, end.day = 140)
Returns environmental kinships for reaction-norm models. The output is a list containing
varCov, the relatedness among environmental covariables, and envCov, the
relatedness among environments. Linear (GBLUP-like), nonlinear Gaussian and arc-cosine
deep kernels are supported.
env_kernel( env.data, Y = NULL, is.scaled = TRUE, sd.tol = 10, digits = 5, tol = 0.001, merge = FALSE, Z_E = NULL, stages = NULL, env.id = "env", gaussian = FALSE, h.gaussian = NULL, deep.kernel = FALSE, deep.layers = 1, QC = FALSE, verbose = TRUE )env_kernel( env.data, Y = NULL, is.scaled = TRUE, sd.tol = 10, digits = 5, tol = 0.001, merge = FALSE, Z_E = NULL, stages = NULL, env.id = "env", gaussian = FALSE, h.gaussian = NULL, deep.kernel = FALSE, deep.layers = 1, QC = FALSE, verbose = TRUE )
env.data |
matrix. Environmental variables (or markers) per environment (or per
genotype-environment combination), typically a |
Y |
data.frame. Phenotypic data set containing environment id, genotype id and trait
value. Only used when |
is.scaled |
boolean. If the environmental data are already mean-centred and scaled
(default |
sd.tol |
numeric. Maximum standard deviation tolerated in quality control. Columns above this value are eliminated. Default 10. |
digits |
numeric. Number of digits used for rounding. Default 5. |
tol |
numeric. Numerical tolerance. Default 1E-3. |
merge |
boolean. If |
Z_E |
matrix. Model matrix for environments, used when |
stages |
vector (character). Names of each stage or time interval. If not |
env.id |
character. Identification of the environment column. Default |
gaussian |
boolean. If |
h.gaussian |
numeric. Bandwidth |
deep.kernel |
boolean. If |
deep.layers |
integer. Number of hidden layers when |
QC |
boolean. If |
verbose |
boolean. If |
Three kernel methods are available.
Linear (GB). , the environmental analogue of
the GBLUP kernel of VanRaden (2008).
Gaussian (GK). , with the squared Euclidean distance
and its median, so the bandwidth is scale-free.
Arc-cosine deep (DK). Emulates a deep neural network with deep.layers hidden
layers (Cuevas et al. 2019); no bandwidth parameter is required.
Note that quality control is now controlled by the explicit QC argument. In earlier
versions it ran only when is.scaled = FALSE, so with the default
is.scaled = TRUE no filtering happened despite sd.tol being documented as
active.
A list with two elements: varCov, the covariable x covariable relatedness, and
envCov, the environment x environment relatedness. When stages is supplied,
each element is itself a named list of kernels, one per stage.
Germano Costa Neto
Cuevas J. et al. (2019). Deep kernel for genomic and near infrared predictions in multi-environment breeding trials. G3 9(9), 2913-2924.
Costa-Neto G., Fritsche-Neto R., Crossa J. (2021). Nonlinear kernels, dominance, and envirotyping data increase the accuracy of genome-based prediction in multi-environment trials. Heredity 126, 92-106.
W_matrix, get_kernel, env_cluster,
T_matrix
data('maizeYield'); data("maizeWTH") ## Environmental covariable matrix (environments x covariables) W.cov <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## 1. Linear (co)variance kernel K.lin <- env_kernel(env.data = W.cov, gaussian = FALSE) round(K.lin$envCov, 2) ## 2. Nonlinear Gaussian kernel K.gau <- env_kernel(env.data = W.cov, gaussian = TRUE) round(K.gau$envCov, 2) ## 3. Arc-cosine deep kernel with 2 hidden layers K.deep <- env_kernel(env.data = W.cov, deep.kernel = TRUE, deep.layers = 2) round(K.deep$envCov, 2) ## 4. One kernel per development stage stages <- c('VE', 'V1_V6', 'V6_VT', 'VT_R1') interval <- c(0, 7, 30, 65, 90) W.stage <- W_matrix(env.data = maizeWTH, var.id = c('FRUE', 'PETP'), by.interval = TRUE, time.window = interval, names.window = stages) K.stage <- env_kernel(env.data = W.stage, stages = stages, gaussian = TRUE) names(K.stage$envCov) ## 5. Feeding a reaction-norm model KE <- list(W = env_kernel(env.data = W.cov)$envCov)data('maizeYield'); data("maizeWTH") ## Environmental covariable matrix (environments x covariables) W.cov <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## 1. Linear (co)variance kernel K.lin <- env_kernel(env.data = W.cov, gaussian = FALSE) round(K.lin$envCov, 2) ## 2. Nonlinear Gaussian kernel K.gau <- env_kernel(env.data = W.cov, gaussian = TRUE) round(K.gau$envCov, 2) ## 3. Arc-cosine deep kernel with 2 hidden layers K.deep <- env_kernel(env.data = W.cov, deep.kernel = TRUE, deep.layers = 2) round(K.deep$envCov, 2) ## 4. One kernel per development stage stages <- c('VE', 'V1_V6', 'V6_VT', 'VT_R1') interval <- c(0, 7, 30, 65, 90) W.stage <- W_matrix(env.data = maizeWTH, var.id = c('FRUE', 'PETP'), by.interval = TRUE, time.window = interval, names.window = stages) K.stage <- env_kernel(env.data = W.stage, stages = stages, gaussian = TRUE) names(K.stage$envCov) ## 5. Feeding a reaction-norm model KE <- list(W = env_kernel(env.data = W.cov)$envCov)
Reproduces Fig. 6c of Della Coletta et al. (2023): for a chosen PC, the loading of every index is plotted against its position in the growing season, with one loess curve per weather factor. This answers "which weather factors, at which point in the season, drive this PC".
env_loading_curve( x, pc = 1, factors = NULL, span = 0.4, points = TRUE, facet = FALSE, title = NULL, verbose = TRUE )env_loading_curve( x, pc = 1, factors = NULL, span = 0.4, points = TRUE, facet = FALSE, title = NULL, verbose = TRUE )
x |
an |
pc |
integer. Which PC to display. Default 1. |
factors |
character vector. Restrict to these weather factors. |
span |
numeric. Loess span. Default 0.4; larger is smoother. |
points |
boolean. Draw the raw per-interval loadings under the curves.
Default |
facet |
boolean. One panel per factor instead of overlaying. Default |
title |
character. Plot title. |
verbose |
boolean. If |
Interpretation. A factor whose curve departs strongly from zero over a stretch of the season contributes heavily to that PC during that window. Sign is arbitrary up to the usual PCA reflection: only relative signs within a PC are meaningful.
Loess caveat. The smoother is cosmetic. With 51 intervals per factor the
curve is well determined, but with few intervals it can imply structure that the
underlying points do not support – keep points = TRUE to see the evidence.
Invisibly, a data.frame of the plotted loadings (index, factor,
day, loading), so the curves can be re-plotted or tabulated.
When ggplot2 is available the ggplot object is attached as the
"plot" attribute.
pc <- env_pca(env_indices(maizeWTH, end.day = 130)) ## All factors on one panel env_loading_curve(pc, pc = 1) ## Temperature factors only, one panel each env_loading_curve(pc, pc = 2, factors = c("T2M", "T2M_MAX", "T2M_MIN"), facet = TRUE) ## Recover the plotted values L <- env_loading_curve(pc, pc = 1) head(L[order(-abs(L$loading)), ])pc <- env_pca(env_indices(maizeWTH, end.day = 130)) ## All factors on one panel env_loading_curve(pc, pc = 1) ## Temperature factors only, one panel each env_loading_curve(pc, pc = 2, factors = c("T2M", "T2M_MAX", "T2M_MIN"), facet = TRUE) ## Recover the plotted values L <- env_loading_curve(pc, pc = 1) head(L[order(-abs(L$loading)), ])
Pearson correlation between each environmental PC and an external per-environment quantity – in the paper, module eigenmarkers; equally, a trait mean, heritability or predictive ability per environment. P-values are FDR-adjusted across PCs, as in the paper.
env_pc_associate( x, y, n.pc = NULL, method = "pearson", p.adjust = "BH", verbose = TRUE )env_pc_associate( x, y, n.pc = NULL, method = "pearson", p.adjust = "BH", verbose = TRUE )
x |
an |
y |
numeric vector or matrix. One value (or column of values) per environment.
Names, or rownames, are matched against the environments in |
n.pc |
integer. How many PCs to test. Default |
method |
character. Correlation method passed to |
p.adjust |
character. Multiple-testing correction. Default |
verbose |
boolean. Report the strongest association. Default |
Power warning. With environments each correlation has
degrees of freedom. At the paper's even gives an
unadjusted , and a handful of PCs tested against a handful of
modules exhausts the evidence quickly. Treat these associations as hypothesis-
generating, not confirmatory, and prefer many environments.
FDR does not cover module selection. The adjustment applied here accounts for testing several PCs. It does not account for the eigenmarker being PC1 of a module that was itself chosen by clustering the same effect matrix. That selection inflates correlations and is invisible to any p-value computed from the observed data. See the worked example below for a permutation check, and for a demonstration that the permutation check is itself insufficient.
A data.frame with column, PC, r, p, p.adj and
n, sorted by adjusted p-value.
pc <- env_pca(env_indices(maizeWTH, end.day = 130)) ## Trait mean per environment ybar <- tapply(maizeYield$value, maizeYield$env, mean) env_pc_associate(pc, ybar) ## ---------------------------------------------------------------------- ## WORKED EXAMPLE: marker effect networks -> eigenmarkers -> environmental PCs ## ## The full analysis chain of Della Coletta et al. (2023), on simulated data ## where the answer is known in advance so the method can be checked. ## ## Planted truth: ## module A markers respond to mid-season TEMPERATURE ## module B markers respond to late-season PRECIPITATION ## module C markers have real but environment-INDEPENDENT effects ## ---------------------------------------------------------------------- ## --- 0. Simulate markers, weather and phenotypes --------------------- set.seed(2026) n.gid <- 160 n.marker <- 90 n.env <- 20 envs <- paste0("E", seq_len(n.env)) drivers <- scale(matrix(rnorm(n.env * 2), n.env, 2)) rownames(drivers) <- envs M <- scale(matrix(rbinom(n.gid * n.marker, 2, 0.5), n.gid, n.marker)) dimnames(M) <- list(paste0("G", seq_len(n.gid)), paste0("m", seq_len(n.marker))) beta <- matrix(0.2, n.marker, n.env) beta[1:30, ] <- rep(0.2 + 0.3 * drivers[, 1], each = 30) beta[31:60, ] <- rep(0.2 + 0.3 * drivers[, 2], each = 30) signal <- M %*% beta Y <- signal + matrix(rnorm(length(signal), sd = 0.3), n.gid, n.env) dimnames(Y) <- list(rownames(M), envs) wth <- expand.grid(daysFromStart = 1:151, env = envs) ie <- match(wth$env, envs) day <- wth$daysFromStart wth$T2M <- 22 + 4 * sin(pi * day / 151) + 3 * drivers[ie, 1] * (day >= 50 & day <= 100) + rnorm(nrow(wth), sd = 0.5) wth$PRECTOT <- exp(1 + 0.6 * drivers[ie, 2] * (day >= 100) + rnorm(nrow(wth), sd = 0.1)) ## --- 1. Marker effects per environment (RR-BLUP) ---------------------- ## Solve via the n x n system: with p markers >> n genotypes this is far ## cheaper than the p x p form and gives the same ridge solution. rrblup_effects <- function(y, Z, h2 = 0.5) { yc <- as.numeric(y) - mean(y) lambda <- ncol(Z) * (1 - h2) / h2 a <- solve(tcrossprod(Z) + lambda * diag(nrow(Z)), yc) as.numeric(crossprod(Z, a)) } beta.hat <- sapply(colnames(Y), function(e) rrblup_effects(Y[, e], M, h2 = 0.55)) dimnames(beta.hat) <- list(colnames(M), colnames(Y)) ## --- 2. Marker effect network -> modules ------------------------------ ## Markers whose effects do not VARY across environments cannot covary with ## anything; screening them out first avoids NA correlations and spurious ## modules. The paper used WGCNA; this is the same idea in base R. eff.sd <- apply(beta.hat, 1, sd) Bk <- beta.hat[eff.sd > quantile(eff.sd, 0.35), , drop = FALSE] adj <- abs(cor(t(Bk)))^6 # soft-thresholding power mods <- cutree(hclust(as.dist(1 - adj), method = "average"), h = 0.92) mods <- mods[mods %in% as.integer(names(table(mods))[table(mods) >= 15])] ## --- 3. Eigenmarkers -------------------------------------------------- ## PC1 of a module's effect profiles, the analogue of a WGCNA module ## eigengene. Sign is arbitrary in PCA, so orient it deterministically -- ## otherwise a module can flip between runs and reverse every downstream ## correlation. eigenmarker <- function(B.mod) { X <- scale(t(B.mod)) em <- prcomp(X, center = FALSE, scale. = FALSE)$x[, 1] if (mean(cor(em, X)) < 0) em <- -em em } ids <- sort(unique(mods)) EM <- sapply(ids, function(m) eigenmarker(Bk[names(mods)[mods == m], , drop = FALSE])) dimnames(EM) <- list(colnames(beta.hat), paste0("ME", ids)) ## --- 4. Environmental PCA and association ----------------------------- W <- env_indices(wth, interval = 3, end.day = 151, statistic.by = c(PRECTOT = "sum")) pc <- env_pca(W) assoc <- env_pc_associate(pc, EM, n.pc = 9, p.adjust = "BH") head(assoc) ## --- 5. Name the drivers of an associated PC (paper Fig. 6c) ---------- k <- as.integer(sub("PC", "", assoc$PC[1])) L <- data.frame(factor = pc$meta$factor, day = pc$meta$day, loading = pc$loadings[, k]) sort(tapply(abs(L$loading), L$factor, mean), decreasing = TRUE) env_loading_curve(pc, pc = k) ## --- 6. Permutation null, and why it is NOT enough -------------------- ## Permuting environment labels of the eigenmarker breaks the ## environment<->module link while preserving both structures. perm.max <- replicate(500, { EMp <- EM[sample(nrow(EM)), , drop = FALSE] rownames(EMp) <- rownames(EM) max(abs(env_pc_associate(pc, EMp, n.pc = 9, verbose = FALSE)$r)) }) quantile(perm.max, 0.95) # compare observed |r| with the permutation null ## A spurious module can pass both FDR adjustment and this permutation test. ## The permutation preserves the module definitions, so it cannot detect a ## module that was spuriously assembled in the first place. Guard against ## this with module stability across resampled genotypes, or by requiring ## an independent set of environments -- not with permutation alone.pc <- env_pca(env_indices(maizeWTH, end.day = 130)) ## Trait mean per environment ybar <- tapply(maizeYield$value, maizeYield$env, mean) env_pc_associate(pc, ybar) ## ---------------------------------------------------------------------- ## WORKED EXAMPLE: marker effect networks -> eigenmarkers -> environmental PCs ## ## The full analysis chain of Della Coletta et al. (2023), on simulated data ## where the answer is known in advance so the method can be checked. ## ## Planted truth: ## module A markers respond to mid-season TEMPERATURE ## module B markers respond to late-season PRECIPITATION ## module C markers have real but environment-INDEPENDENT effects ## ---------------------------------------------------------------------- ## --- 0. Simulate markers, weather and phenotypes --------------------- set.seed(2026) n.gid <- 160 n.marker <- 90 n.env <- 20 envs <- paste0("E", seq_len(n.env)) drivers <- scale(matrix(rnorm(n.env * 2), n.env, 2)) rownames(drivers) <- envs M <- scale(matrix(rbinom(n.gid * n.marker, 2, 0.5), n.gid, n.marker)) dimnames(M) <- list(paste0("G", seq_len(n.gid)), paste0("m", seq_len(n.marker))) beta <- matrix(0.2, n.marker, n.env) beta[1:30, ] <- rep(0.2 + 0.3 * drivers[, 1], each = 30) beta[31:60, ] <- rep(0.2 + 0.3 * drivers[, 2], each = 30) signal <- M %*% beta Y <- signal + matrix(rnorm(length(signal), sd = 0.3), n.gid, n.env) dimnames(Y) <- list(rownames(M), envs) wth <- expand.grid(daysFromStart = 1:151, env = envs) ie <- match(wth$env, envs) day <- wth$daysFromStart wth$T2M <- 22 + 4 * sin(pi * day / 151) + 3 * drivers[ie, 1] * (day >= 50 & day <= 100) + rnorm(nrow(wth), sd = 0.5) wth$PRECTOT <- exp(1 + 0.6 * drivers[ie, 2] * (day >= 100) + rnorm(nrow(wth), sd = 0.1)) ## --- 1. Marker effects per environment (RR-BLUP) ---------------------- ## Solve via the n x n system: with p markers >> n genotypes this is far ## cheaper than the p x p form and gives the same ridge solution. rrblup_effects <- function(y, Z, h2 = 0.5) { yc <- as.numeric(y) - mean(y) lambda <- ncol(Z) * (1 - h2) / h2 a <- solve(tcrossprod(Z) + lambda * diag(nrow(Z)), yc) as.numeric(crossprod(Z, a)) } beta.hat <- sapply(colnames(Y), function(e) rrblup_effects(Y[, e], M, h2 = 0.55)) dimnames(beta.hat) <- list(colnames(M), colnames(Y)) ## --- 2. Marker effect network -> modules ------------------------------ ## Markers whose effects do not VARY across environments cannot covary with ## anything; screening them out first avoids NA correlations and spurious ## modules. The paper used WGCNA; this is the same idea in base R. eff.sd <- apply(beta.hat, 1, sd) Bk <- beta.hat[eff.sd > quantile(eff.sd, 0.35), , drop = FALSE] adj <- abs(cor(t(Bk)))^6 # soft-thresholding power mods <- cutree(hclust(as.dist(1 - adj), method = "average"), h = 0.92) mods <- mods[mods %in% as.integer(names(table(mods))[table(mods) >= 15])] ## --- 3. Eigenmarkers -------------------------------------------------- ## PC1 of a module's effect profiles, the analogue of a WGCNA module ## eigengene. Sign is arbitrary in PCA, so orient it deterministically -- ## otherwise a module can flip between runs and reverse every downstream ## correlation. eigenmarker <- function(B.mod) { X <- scale(t(B.mod)) em <- prcomp(X, center = FALSE, scale. = FALSE)$x[, 1] if (mean(cor(em, X)) < 0) em <- -em em } ids <- sort(unique(mods)) EM <- sapply(ids, function(m) eigenmarker(Bk[names(mods)[mods == m], , drop = FALSE])) dimnames(EM) <- list(colnames(beta.hat), paste0("ME", ids)) ## --- 4. Environmental PCA and association ----------------------------- W <- env_indices(wth, interval = 3, end.day = 151, statistic.by = c(PRECTOT = "sum")) pc <- env_pca(W) assoc <- env_pc_associate(pc, EM, n.pc = 9, p.adjust = "BH") head(assoc) ## --- 5. Name the drivers of an associated PC (paper Fig. 6c) ---------- k <- as.integer(sub("PC", "", assoc$PC[1])) L <- data.frame(factor = pc$meta$factor, day = pc$meta$day, loading = pc$loadings[, k]) sort(tapply(abs(L$loading), L$factor, mean), decreasing = TRUE) env_loading_curve(pc, pc = k) ## --- 6. Permutation null, and why it is NOT enough -------------------- ## Permuting environment labels of the eigenmarker breaks the ## environment<->module link while preserving both structures. perm.max <- replicate(500, { EMp <- EM[sample(nrow(EM)), , drop = FALSE] rownames(EMp) <- rownames(EM) max(abs(env_pc_associate(pc, EMp, n.pc = 9, verbose = FALSE)$r)) }) quantile(perm.max, 0.95) # compare observed |r| with the permutation null ## A spurious module can pass both FDR adjustment and this permutation test. ## The permutation preserves the module definitions, so it cannot detect a ## module that was spuriously assembled in the first place. Guard against ## this with module stability across resampled genotypes, or by requiring ## an independent set of environments -- not with permutation alone.
Runs prcomp on the environment x index matrix, as in
Della Coletta et al. (2023), and returns scores, loadings and variance
explained in a form the plotting functions in this module can consume.
env_pca( W, center = TRUE, scale. = TRUE, rank. = NULL, drop.constant = TRUE, verbose = TRUE )env_pca( W, center = TRUE, scale. = TRUE, rank. = NULL, drop.constant = TRUE, verbose = TRUE )
W |
matrix. Environment x index matrix, typically from |
center |
boolean. Centre the indices. Default |
scale. |
boolean. Scale the indices to unit variance. Default |
rank. |
integer or NULL. Maximum number of PCs to compute. |
drop.constant |
boolean. If |
verbose |
boolean. Print a summary. Default |
Scaling. The paper reports only that prcomp was used. Because the
17 factors are on incomparable scales – radiation in MJ/m2/day, precipitation in mm,
FRUE unitless in [0,1] – an unscaled PCA is dominated by the highest-variance
factor and the leading PCs largely describe that one variable. scale. = TRUE
is therefore the default here. Set scale. = FALSE for a covariance-matrix PCA.
Rank. With environments and indices, at most
PCs carry variance. With the 9 environments of the paper that is 8, so
the reported "ninth PC" explaining is effectively numerical noise.
Expect very few informative PCs unless you have many environments.
A list of class "env_pca":
scoresenvironment x PC matrix (prcomp$x)
loadingsindex x PC matrix (prcomp$rotation)
variancedata.frame with PC, sd, prop, cum
prcompthe underlying prcomp object
droppedindices removed as constant
metadata.frame mapping each index to its factor, interval and day
env_indices, env_pca_biplot,
env_loading_curve
data("maizeWTH") W <- env_indices(maizeWTH, end.day = 130) pc <- env_pca(W) head(pc$variance) env_pca_scree(pc) env_pca_biplot(pc)data("maizeWTH") W <- env_indices(maizeWTH, end.day = 130) pc <- env_pca(W) head(pc$variance) env_pca_scree(pc) env_pca_biplot(pc)
Scatter of environments on two PCs, reproducing Fig. 1c of Della Coletta et al. (2023), where each point is one growth environment labelled by its code.
env_pca_biplot( x, pc = c(1, 2), label = TRUE, group = NULL, title = "Environments in PC space", verbose = TRUE )env_pca_biplot( x, pc = c(1, 2), label = TRUE, group = NULL, title = "Environments in PC space", verbose = TRUE )
x |
an |
pc |
integer vector of length 2. Which PCs to plot. Default |
label |
boolean. Draw environment names. Default |
group |
factor or named vector. Optional grouping used to colour points, e.g. a mega-environment assignment. Names are matched to environment names. |
title |
character. Plot title. |
verbose |
boolean. If |
A ggplot object when ggplot2 is available (invisibly), otherwise
NULL after drawing a base plot.
pc <- env_pca(env_indices(maizeWTH, end.day = 130)) env_pca_biplot(pc) env_pca_biplot(pc, pc = c(1, 3))pc <- env_pca(env_indices(maizeWTH, end.day = 130)) env_pca_biplot(pc) env_pca_biplot(pc, pc = c(1, 3))
Plots the proportion of variance explained per PC, the visualisation the paper used to decide how many PCs to retain.
env_pca_scree( x, n.pc = NULL, cumulative = TRUE, title = "Variance explained by environmental PCs", verbose = TRUE )env_pca_scree( x, n.pc = NULL, cumulative = TRUE, title = "Variance explained by environmental PCs", verbose = TRUE )
x |
an |
n.pc |
integer. How many PCs to show. Default |
cumulative |
boolean. Overlay the cumulative proportion. Default |
title |
character. Plot title. |
verbose |
boolean. If |
A ggplot object when ggplot2 is available (invisibly), otherwise
NULL after drawing a base plot.
pc <- env_pca(env_indices(maizeWTH, end.day = 130)) env_pca_scree(pc)pc <- env_pca(env_indices(maizeWTH, end.day = 130)) env_pca_scree(pc)
Accumulates daily thermal time from planting and cuts each environment's season at crop-specific developmental thresholds, so that every downstream interval-based routine compares LIKE DEVELOPMENTAL STATES across sites rather than like calendar dates.
The returned stage column is a drop-in replacement for the
interval produced by time.window in W_matrix(),
summaryWTH() and env_typing().
env_phenology( env.data, env.id = "env", day.id = "daysFromStart", Tmax = "T2M_MAX", Tmin = "T2M_MIN", crop = NULL, stages = NULL, Tbase = NULL, Tupper = NULL, method = c("gdd", "baskerville", "trapezoid"), Tmaxdev = 45, photoperiod = FALSE, lat = "LAT", daylength.id = NULL, p.opt = NULL, p.sens = 0.02, day.type = NULL, planting.id = NULL, merge = TRUE, verbose = TRUE )env_phenology( env.data, env.id = "env", day.id = "daysFromStart", Tmax = "T2M_MAX", Tmin = "T2M_MIN", crop = NULL, stages = NULL, Tbase = NULL, Tupper = NULL, method = c("gdd", "baskerville", "trapezoid"), Tmaxdev = 45, photoperiod = FALSE, lat = "LAT", daylength.id = NULL, p.opt = NULL, p.sens = 0.02, day.type = NULL, planting.id = NULL, merge = TRUE, verbose = TRUE )
env.data |
data.frame. Daily weather, typically a |
env.id |
character. Environment-id column. Default |
day.id |
character. Day-counter column. Default |
Tmax, Tmin
|
character. Daily maximum/minimum temperature columns.
Defaults |
crop |
character. Name of a built-in template, e.g. |
stages |
named numeric. Cumulative thermal time (C d) at the END of
each stage, strictly increasing, e.g.
|
Tbase, Tupper
|
numeric. Cardinal temperatures for thermal time. Override the crop template when supplied. |
method |
character. Thermal-time method: |
Tmaxdev |
numeric. Absolute developmental maximum, used by
|
photoperiod |
boolean. If |
lat |
character or numeric. Latitude column name or a numeric vector,
used to derive daylength when |
daylength.id |
character. Existing daylength column (h), e.g. |
p.opt, p.sens
|
numeric. Optimum daylength (h) and sensitivity
(fractional slowdown per hour of deviation). When |
day.type |
character. |
planting.id |
character. Optional column giving planting date, used to
reset accumulation. If |
merge |
boolean. If |
verbose |
boolean. Print a progress banner. |
Which method. "gdd" is the convention and what
param_temperature() already computes; use it for comparability.
"baskerville" is materially better where daily range straddles
Tbase – high-elevation or early-spring sites – because the simple
mean then understates accumulation. "trapezoid" is the only option
that penalises supra-optimal heat, relevant for heat-stress work.
Photothermal time. Maize, sorghum and rice are quantitative
short-day plants: long days delay flowering. With photoperiod = TRUE
the daily increment is scaled by a linear daylength factor, which keeps
tropical and temperate plantings on a common developmental scale. The
default p.sens = 0.02 is deliberately mild; calibrate it before
relying on the absolute stage dates.
Caveat. These thresholds are cultivar-generic. Maturity group alone
moves maize R6 by 300-500 C d. Treat the stage boundaries as a consistent
relative scale, not as a validated phenology prediction, unless you have
calibrated stages against observed dates.
A data.frame with these columns added:
daily thermal time (C d), or photothermal time if
photoperiod = TRUE
cumulative thermal time from planting
ordered factor, the developmental stage of that day
relative position within the stage, in [0, 1)
day counter within the stage (1-based)
The attribute "phenology" carries the stage table, the method and the
cardinal temperatures. Environments whose season ends before the last
threshold get NA for the stages never reached, and a warning names
them – silently recycling the final stage would fabricate development that
the weather does not support.
Germano Costa Neto
McMaster, G.S. & Wilhelm, W.W. (1997) Growing degree-days: one equation, two interpretations. Agric. For. Meteorol. 87(4), 291-300.
Baskerville, G.L. & Emin, P. (1969) Rapid estimation of heat accumulation from maximum and minimum temperatures. Ecology 50(3), 514-517.
Abendroth, L.J. et al. (2011) Corn Growth and Development. PMR 1009, Iowa State University Extension.
param_temperature, W_matrix,
env_risk_profile, summaryWTH
data("maizeWTH") ## 1. Default maize staging on cumulative GDD ph <- env_phenology(maizeWTH, crop = "maize") table(ph$env, ph$stage) ## 2. Photothermal time, daylength taken from param_radiation()'s N column ph2 <- env_phenology(processWTH(maizeWTH), crop = "maize", photoperiod = TRUE, daylength.id = "N") ## 3. Custom stages and the Baskerville-Emin method ph3 <- env_phenology(maizeWTH, stages = c(veg = 500, flow = 1200, fill = 2200), Tbase = 8, Tupper = 32, method = "baskerville") ## 4. Hand the stage factor to the existing covariable machinery. ## This is the point of the function: W columns now align on ## development instead of on calendar date. W <- W_matrix(env.data = ph, var.id = c("T2M", "PRECTOT"), by.interval = TRUE, time.window = NULL)data("maizeWTH") ## 1. Default maize staging on cumulative GDD ph <- env_phenology(maizeWTH, crop = "maize") table(ph$env, ph$stage) ## 2. Photothermal time, daylength taken from param_radiation()'s N column ph2 <- env_phenology(processWTH(maizeWTH), crop = "maize", photoperiod = TRUE, daylength.id = "N") ## 3. Custom stages and the Baskerville-Emin method ph3 <- env_phenology(maizeWTH, stages = c(veg = 500, flow = 1200, fill = 2200), Tbase = 8, Tupper = 32, method = "baskerville") ## 4. Hand the stage factor to the existing covariable machinery. ## This is the point of the function: W columns now align on ## development instead of on calendar date. W <- W_matrix(env.data = ph, var.id = c("T2M", "PRECTOT"), by.interval = TRUE, time.window = NULL)
Quantifies how OFTEN each stress envirotype occurs at a site, by
developmental stage, across many historical seasons. Where
env_typing() describes the environments you actually observed,
env_risk_profile() estimates the underlying climatology those
environments were drawn from – the frequency, not the realisation.
This is the quantity a breeding programme needs to weight its testing network: a mega-environment that appears in 1 season out of 20 should not carry the same weight as one that appears in 12.
env_risk_profile( env.data, site.id = "env", date.id = "YYYYMMDD", var.id = NULL, years = NULL, planting = NULL, season.length = 150, cardinals = NULL, by.stage = TRUE, crop = "maize", stages = NULL, Tbase = NULL, Tupper = NULL, method = "gdd", min.days = 30L, return.seasons = FALSE, verbose = TRUE )env_risk_profile( env.data, site.id = "env", date.id = "YYYYMMDD", var.id = NULL, years = NULL, planting = NULL, season.length = 150, cardinals = NULL, by.stage = TRUE, crop = "maize", stages = NULL, Tbase = NULL, Tupper = NULL, method = "gdd", min.days = 30L, return.seasons = FALSE, verbose = TRUE )
env.data |
data.frame. Multi-year daily weather for one or more sites,
typically |
site.id |
character. Site-id column. Default |
date.id |
character. Date column ( |
var.id |
vector (character). Variables to profile. Defaults to those
present among the names of |
years |
vector (numeric). Seasons to include. Defaults to every year present with a complete season. |
planting |
character, |
season.length |
numeric. Days after planting retained per season when phenology is not used. Default 150. |
cardinals |
named list. Per-variable |
by.stage |
boolean. If |
crop, stages, Tbase, Tupper, method
|
Passed to |
min.days |
integer. Minimum days a site-year must contribute to be kept. Default 30. |
return.seasons |
boolean. If |
verbose |
boolean. Print progress. |
Two frequencies are reported and they answer different questions.
freq is the proportion of SEASONS in which the class occurred at
least once – the probability of encountering that stress. mean_days
is the expected NUMBER OF DAYS per season – the intensity. A site can have
freq = 1.0 for mild heat while averaging only 3 days of it; that is a
very different target from one averaging 40 days.
Return period is simply 1 / freq and inherits all the usual
caveats: it is an average recurrence interval estimated from a finite
record, not a schedule. With 20 years of data a return period above ~10
seasons is barely distinguishable from noise. The function will not print a
return period longer than the record.
Stationarity. Frequencies computed over 1990-2024 describe that
period. If the site is warming, they are already an underestimate of future
heat risk. Use project_risk() with a scenario to see the shift.
An object of class "env_risk": a list with
data.frame of site x stage x variable x class with
n_years, n_occur, freq (probability a season shows
that class at all), and mean_days (expected days per season)
data.frame of site x stage x variable with the mean, sd and 10th/90th percentile of the variable across seasons
data.frame with 1 / freq, the expected number
of seasons between occurrences of each stress class
per-site-year detail if return.seasons = TRUE
provenance
Germano Costa Neto
Chapman, S.C. et al. (2000) Using crop simulation to generate genotype by environment interaction effects for sorghum in water-limited environments. Aust. J. Agric. Res. 51(2), 209-221.
Heinemann, A.B. et al. (2015) Drought impact on rainfed common bean production areas in Brazil. Agric. For. Meteorol. 225, 1-12.
env_phenology, env_typing,
tpe_weights, project_risk
## 30 seasons of weather for three sites sites <- data.frame(env = c("PIRA", "SETE", "NMAC"), lat = c(-22.7, -19.4, -21.2), lon = c(-47.6, -44.2, -45.2)) wth <- get_weather(env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "1994-01-01", end.day = "2024-12-31") wth <- processWTH(wth) ## Risk of heat and drought at flowering, sown 15 October each year rp <- env_risk_profile(wth, site.id = "env", planting = "10-15", var.id = c("T2M_MAX", "PETP"), crop = "maize", by.stage = TRUE) rp ## How often is flowering heat-stressed at each site? subset(rp$frequency, stage == "R1" & class == "heat_severe") ## Weights for a target population of environments w <- tpe_weights(rp, stage = "R1", var = "PETP")## 30 seasons of weather for three sites sites <- data.frame(env = c("PIRA", "SETE", "NMAC"), lat = c(-22.7, -19.4, -21.2), lon = c(-47.6, -44.2, -45.2)) wth <- get_weather(env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "1994-01-01", end.day = "2024-12-31") wth <- processWTH(wth) ## Risk of heat and drought at flowering, sown 15 October each year rp <- env_risk_profile(wth, site.id = "env", planting = "10-15", var.id = c("T2M_MAX", "PETP"), crop = "maize", by.stage = TRUE) rp ## How often is flowering heat-stressed at each site? subset(rp$frequency, stage == "R1" & class == "heat_severe") ## Weights for a target population of environments w <- tpe_weights(rp, stage = "R1", var = "PETP")
Groups environments by their target metrics (Y: phenotypic means, heritabilities,
selection accuracies, etc.) using K-means, then trains a Random Forest to predict those
clusters from the environmental covariable matrix (W). The Random Forest variable
importance ranks which environmental variables best explain the target-based grouping of
environments. The number of clusters can be supplied by the user or optimised automatically
with the average silhouette width.
env_target_importance( Y, W, k = NULL, k.max = 10, scale.Y = TRUE, ntree = 1000, importance.type = c("gini", "permutation"), nstart = 25, seed = NULL, save = FALSE, dir.path = NULL, verbose = TRUE )env_target_importance( Y, W, k = NULL, k.max = 10, scale.Y = TRUE, ntree = 1000, importance.type = c("gini", "permutation"), nstart = 25, seed = NULL, save = FALSE, dir.path = NULL, verbose = TRUE )
Y |
numeric vector, matrix or data.frame. Target metrics with environments in rows
(e.g. adjusted means, |
W |
matrix or data.frame. Environmental covariables with environments in rows and
variables in columns (e.g. a |
k |
integer or NULL. Number of clusters. If |
k.max |
integer. Maximum number of clusters tested when |
scale.Y |
boolean. If |
ntree |
integer. Number of trees in the Random Forest. Default 1000. |
importance.type |
character. |
nstart |
integer. Number of random starts for K-means. Default 25. |
seed |
integer or NULL. Optional seed for reproducibility. |
save |
boolean. If |
dir.path |
character. Output directory when |
verbose |
boolean. If |
This is a supervised counterpart of env_cluster: instead of grouping
environments by their enviromic profile, environments are grouped by what the breeder
cares about (yield level, heritability, accuracy) and the enviromic data are then asked to
explain that grouping. Variables at the top of importance are candidate drivers of the
observed environmental stratification and good candidates for env_threshold_gxe.
Permutation importance ("permutation") is generally preferred when covariables differ
in scale or in number of categories, since MeanDecreaseGini is biased towards high-cardinality
predictors.
A list with:
clusters: named integer vector of the cluster of each environment.
k: the number of clusters used.
silhouette: data.frame of average silhouette width per tested k
(NULL if k was given).
kmeans: the kmeans object.
randomForest: the fitted randomForest object.
importance: data.frame of environmental variables ranked by importance (descending).
Germano Costa Neto
Costa-Neto, G., da Matta, D., Fernandes, I. K., & Heinemann, A. B. (2023). Environmental clusters defining breeding zones for tropical irrigated rice in Brazil. Agronomy Journal, 115(5). doi:10.1002/agj2.21481
env_cluster, W_matrix
if (requireNamespace("randomForest", quietly = TRUE)) { data("maizeYield"); data("maizeWTH") ## Environment-level target: mean grain yield per environment Y <- tapply(maizeYield$value, maizeYield$env, mean) ## Environmental covariables (environments x covariables) W <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## Which environmental variables best explain the target-based grouping? out <- env_target_importance(Y = Y, W = W, seed = 1) out$k out$clusters head(out$importance) ## Permutation importance and a fixed number of clusters out2 <- env_target_importance(Y = Y, W = W, k = 2, importance.type = "permutation", seed = 1) head(out2$importance) ## Multi-trait target (e.g. mean and heritability per environment) Ymat <- cbind(mean = Y, sd = tapply(maizeYield$value, maizeYield$env, stats::sd)) out3 <- env_target_importance(Y = Ymat, W = W, seed = 1) out3$importance }if (requireNamespace("randomForest", quietly = TRUE)) { data("maizeYield"); data("maizeWTH") ## Environment-level target: mean grain yield per environment Y <- tapply(maizeYield$value, maizeYield$env, mean) ## Environmental covariables (environments x covariables) W <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("T2M", "T2M_MAX", "PRECTOT"), statistic = "mean") ## Which environmental variables best explain the target-based grouping? out <- env_target_importance(Y = Y, W = W, seed = 1) out$k out$clusters head(out$importance) ## Permutation importance and a fixed number of clusters out2 <- env_target_importance(Y = Y, W = W, k = 2, importance.type = "permutation", seed = 1) head(out2$importance) ## Multi-trait target (e.g. mean and heritability per environment) Ymat <- cbind(mean = Y, sd = tapply(maizeYield$value, maizeYield$env, stats::sd)) out3 <- env_target_importance(Y = Ymat, W = W, seed = 1) out3$importance }
Returns environmental typologies (envirotypes) that can be used as envirotype
markers. Typologies are given by cardinals (discrete intervals for each variable), by
empirical quantiles, or – when envirotype_mining = TRUE – learned from the data:
for each variable a K-means clustering is run over a grid of candidate k values and
the optimal number of envirotypes is selected by the Calinski-Harabasz (variance ratio)
criterion.
env_typing( env.data, var.id, env.id, cardinals = NULL, days.id = NULL, time.window = NULL, names.window = NULL, quantiles = NULL, id.names = NULL, by.interval = FALSE, scale = FALSE, format = NULL, ratio = FALSE, joint = FALSE, joint.by.interval = TRUE, combine.features = FALSE, combine.max.order = 2L, envirotype_mining = FALSE, k.range = 2:10, nstart = 25, iter.max = 100, seed = 1234, plot_envirotypes = FALSE, min.window = 2L, verbose = TRUE )env_typing( env.data, var.id, env.id, cardinals = NULL, days.id = NULL, time.window = NULL, names.window = NULL, quantiles = NULL, id.names = NULL, by.interval = FALSE, scale = FALSE, format = NULL, ratio = FALSE, joint = FALSE, joint.by.interval = TRUE, combine.features = FALSE, combine.max.order = 2L, envirotype_mining = FALSE, k.range = 2:10, nstart = 25, iter.max = 100, seed = 1234, plot_envirotypes = FALSE, min.window = 2L, verbose = TRUE )
env.data |
data.frame of environmental variables from |
var.id |
character. Which variables will be used in the analysis. |
env.id |
character. Column used as id for environments. |
cardinals |
list (numeric). Cardinal thresholds per variable. If |
days.id |
character. Name of the column indicating the days from start. |
time.window |
vector (numeric). If |
names.window |
vector (character). If |
quantiles |
vector (numeric). Probability quantiles used when |
id.names |
vector (character). Columns used as id for the environmental variables. |
by.interval |
boolean. Compute temporal intervals inside each environment. Default |
scale |
boolean. If |
format |
character. Output shape, |
ratio |
boolean. If |
joint |
boolean. Only used when |
joint.by.interval |
boolean. Only used when |
combine.features |
boolean. Only valid when |
combine.max.order |
integer or |
envirotype_mining |
boolean. If |
k.range |
integer vector. Candidate numbers of envirotypes tested when mining. Default |
nstart |
numeric. Random starts passed to |
iter.max |
numeric. Maximum iterations passed to |
seed |
numeric. Random seed for reproducible K-means. Default 1234; |
plot_envirotypes |
boolean. If |
min.window |
integer. Minimum number of observations required in the open-ended
trailing interval. Default 2; use 0 or |
verbose |
boolean. If |
The Calinski-Harabasz index for a partition into groups of observations is
where BGSS is the between-group and WGSS the within-group sum of squares. The
maximising CH is retained. Clusters are relabelled in increasing order of their centres so
that Envirotype_<var>_00001 is always the lowest range of the variable. For joint
(multivariate) mining there is no natural ordering, so centroids are ordered along their
first principal component and variables are standardised within the window before
clustering so that no feature dominates the Euclidean distance.
If envirotype_mining = FALSE (default) and plot_envirotypes = FALSE: a
data.frame (or matrix, if format = 'wide') of envirotype frequencies.
Otherwise a list with:
typologiesdata.frame (long) or matrix (wide) of envirotype frequencies.
envirotype_descriptiondata.frame describing the numeric range captured by
each envirotype: variable, label, cluster index, n, min, max,
mean, sd, median and the lower/upper cut boundaries.
mining_summarydata.frame with the optimal k, the CH index at the
optimum and the full CH profile over k.range.
feature_combinationsonly when combine.features = TRUE: a
data.frame mapping each generic combination id to its number of features
(n_features), the features themselves and the time intervals.
plotthe plot object, or NULL.
Germano Costa Neto
W_matrix, env_kernel, T_matrix
data("maizeWTH") env.data <- maizeWTH[maizeWTH$daysFromStart < 100, ] ## 1. Generic time intervals env_typing(env.data = env.data, env.id = 'env', var.id = 'T2M', by.interval = TRUE) ## 2. Specific time intervals with names env_typing(env.data = env.data, env.id = 'env', var.id = 'T2M', by.interval = TRUE, time.window = c(0, 15, 35, 65, 90), names.window = c('1-initial growing', '2-leaf expansion I', '3-leaf expansion II', '4-flowering', '5-grain filling')) ## 3. Cardinal (ecophysiological) thresholds env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT'), cardinals = list(T2M = c(0, 9, 22, 32, 45), PRECTOT = c(0, 5, 10, 25, 100))) ## 4. Data-driven univariate mining (K-means + Calinski-Harabasz) out <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT'), envirotype_mining = TRUE, k.range = 2:8) out$typologies out$envirotype_description out$mining_summary ## 5. Joint (multi-variable) envirotypes per development stage outj <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT'), by.interval = TRUE, time.window = c(0, 30, 60, 90), envirotype_mining = TRUE, joint = TRUE) head(outj$envirotype_description) ## 6. Relative frequencies, wide format (ready for env_kernel) ET <- env_typing(env.data = env.data, env.id = 'env', var.id = 'T2M', format = 'wide', ratio = TRUE) ## 7. Envirotypes for feature combinations (singles + pairs by default) outc <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT', 'VPD'), envirotype_mining = TRUE, combine.features = TRUE) outc$feature_combinations # generic id -> which variables head(outc$envirotype_description) ## include triples too (combine.max.order = 3), or NULL for all orders outc3 <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT', 'VPD'), envirotype_mining = TRUE, combine.features = TRUE, combine.max.order = 3)data("maizeWTH") env.data <- maizeWTH[maizeWTH$daysFromStart < 100, ] ## 1. Generic time intervals env_typing(env.data = env.data, env.id = 'env', var.id = 'T2M', by.interval = TRUE) ## 2. Specific time intervals with names env_typing(env.data = env.data, env.id = 'env', var.id = 'T2M', by.interval = TRUE, time.window = c(0, 15, 35, 65, 90), names.window = c('1-initial growing', '2-leaf expansion I', '3-leaf expansion II', '4-flowering', '5-grain filling')) ## 3. Cardinal (ecophysiological) thresholds env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT'), cardinals = list(T2M = c(0, 9, 22, 32, 45), PRECTOT = c(0, 5, 10, 25, 100))) ## 4. Data-driven univariate mining (K-means + Calinski-Harabasz) out <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT'), envirotype_mining = TRUE, k.range = 2:8) out$typologies out$envirotype_description out$mining_summary ## 5. Joint (multi-variable) envirotypes per development stage outj <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT'), by.interval = TRUE, time.window = c(0, 30, 60, 90), envirotype_mining = TRUE, joint = TRUE) head(outj$envirotype_description) ## 6. Relative frequencies, wide format (ready for env_kernel) ET <- env_typing(env.data = env.data, env.id = 'env', var.id = 'T2M', format = 'wide', ratio = TRUE) ## 7. Envirotypes for feature combinations (singles + pairs by default) outc <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT', 'VPD'), envirotype_mining = TRUE, combine.features = TRUE) outc$feature_combinations # generic id -> which variables head(outc$envirotype_description) ## include triples too (combine.max.order = 3), or NULL for all orders outc3 <- env_typing(env.data = env.data, env.id = 'env', var.id = c('T2M', 'PRECTOT', 'VPD'), envirotype_mining = TRUE, combine.features = TRUE, combine.max.order = 3)
Genomes to fields (G2F) is a multi-institutional, public collaborative to develop information and tools that support the translation of maize (Zea mays L.) genomic information into relevant phenotypes for the benefit of growers, consumers, and society. Building on existing maize genome sequence resources, the project focuses on developing approaches to improve phenomic predictability and facilitate the development and deployment of tools and resources that help address fundamental problems of sustainable agricultural productivity. Specific projects within G2F involve collaboration from research fields such as genetics, genomics, plant physiology, agronomy, climatology and crop modeling, computational sciences, statistics, and engineering. See https://bmcresnotes.biomedcentral.com/articles/10.1186/s13104-020-4922-8.
Here we used the geographic coordinates from 93 environments (diverse locations across 4 years)
data(G2F_2014_17)data(G2F_2014_17)
A data.frame containing environmental identifications (env), geographic coordinates (lat, lon) and the time window (start, end) given in YYYY-MM-DD.
data.frame file
require(EnvRtype) data(G2F_2014_17) head(G2F_2014_17) # let's use the first 10 locations only data = G2F_2014_17 env = data$env[1:10] lon = data$lon[1:10] lat = data$lat[1:10] start = data$start[1:10] end = data$end[1:10] # get_weather() queries the NASA POWER web service weather.data = EnvRtype::get_weather(env.id = env, lat = lat, lon = lon, start.day = start, end.day = end) head(weather.data)require(EnvRtype) data(G2F_2014_17) head(G2F_2014_17) # let's use the first 10 locations only data = G2F_2014_17 env = data$env[1:10] lon = data$lon[1:10] lat = data$lat[1:10] start = data$start[1:10] end = data$end[1:10] # get_weather() queries the NASA POWER web service weather.data = EnvRtype::get_weather(env.id = env, lat = lat, lon = lon, start.day = start, end.day = end) head(weather.data)
Extracts the FAO/IIASA Global Agro-Ecological Zone (GAEZ v4) class at a set of
coordinates. By default it reads the GAEZ 57-class raster bundled with
the package, so it works offline with no arguments beyond the coordinates.
Companion to get_weather and get_soil.
get_AEZ( env.id, lat, lon, file = NULL, url = aez_default_url(), dir.path = NULL, legend = NULL, buffer = 0, overwrite = FALSE, timeout = 1800L, verbose = TRUE )get_AEZ( env.id, lat, lon, file = NULL, url = aez_default_url(), dir.path = NULL, legend = NULL, buffer = 0, overwrite = FALSE, timeout = 1800L, verbose = TRUE )
env.id |
character vector. Environment/site identifiers. |
lat |
numeric vector. Latitude in decimal degrees (WGS84), same length as
|
lon |
numeric vector. Longitude in decimal degrees (WGS84), same length as
|
file |
character. Path to a GAEZ GeoTIFF already on disk. If |
url |
character. Download URL for a GAEZ raster, used only when |
dir.path |
character. Where to cache a downloaded raster. Default
|
legend |
data.frame or NULL. Lookup table with at least |
buffer |
numeric. If greater than 0, the radius in metres of a circular neighbourhood summarised around each point (majority class and its share), rather than a single-cell lookup. Default 0. |
overwrite |
boolean. Re-download even if the cached file exists. Default
|
timeout |
integer. Seconds allowed for the download. Default 1800; GAEZ rasters are large and R's 60-second default will abort them. |
verbose |
boolean. Progress messages. Default |
Where the raster comes from. GAEZ v4 has no documented per-point API:
it publishes whole-globe GeoTIFFs through a JavaScript portal. get_AEZ
is therefore a raster-extract tool and resolves its raster in three steps –
an explicit file, else the layer bundled under inst/extdata, else
a download from url. The bundled layer is the GAEZ v4 57-class AEZ
historical baseline (CRUTS32_Hist_8110, ~5 arc-min), which makes the
default call fully offline and reproducible.
The default URL is unverified. It is only used when both file
and the bundled raster are unavailable. FAO has reorganised GAEZ download paths
between releases and the portal is JavaScript-rendered, so no stable path could
be confirmed. If a download is triggered and fails:
download the AEZ classification layer manually,
pass the file with file = "path/to/layer.tif".
Coordinates must be WGS84 lon/lat. They are reprojected to the raster's CRS automatically, but only if the raster declares one. A raster with an undefined CRS triggers an error rather than a silent mis-extraction.
Interpreting buffer. A single-cell lookup at ~9 km resolution can
be misleading near a zone boundary. With buffer > 0 the returned
majority_share shows how homogeneous the neighbourhood is; a value near
1 means the site sits well inside one zone, while 0.4 means the classification
is effectively arbitrary at that location.
A data.frame with one row per environment:
envenvironment id
lat, lon
coordinates as supplied
aez_coderaster cell value
aez_label, thermal, moisture, arable
from the legend, when one is used
majority_sharewhen buffer > 0, the proportion of cells
in the neighbourhood holding the majority class – a purity measure
The raster path used is attached as the "source" attribute.
FAO & IIASA (2021). Global Agro-Ecological Zones (GAEZ v4). Rome, FAO. https://gaez.fao.org
aez_legend, get_weather, get_soil
sites <- data.frame( env = c("Ames_IA", "Lincoln_NE", "Piracicaba_BR"), lat = c(42.03, 40.81, -22.71), lon = c(-93.62, -96.68, -47.63)) ## Default: uses the raster bundled with the package (offline) aez <- get_AEZ(sites$env, sites$lat, sites$lon) aez ## Check zone purity in a 25 km neighbourhood aez25 <- get_AEZ(sites$env, sites$lat, sites$lon, buffer = 25000) aez25[, c("env", "aez_label", "majority_share")] ## Raw codes, no labelling get_AEZ(sites$env, sites$lat, sites$lon, legend = NA) ## Not run: ## A different GAEZ layer you downloaded yourself get_AEZ(sites$env, sites$lat, sites$lon, file = "GAEZ_AEZ_scenario.tif") ## End(Not run)sites <- data.frame( env = c("Ames_IA", "Lincoln_NE", "Piracicaba_BR"), lat = c(42.03, 40.81, -22.71), lon = c(-93.62, -96.68, -47.63)) ## Default: uses the raster bundled with the package (offline) aez <- get_AEZ(sites$env, sites$lat, sites$lon) aez ## Check zone purity in a 25 km neighbourhood aez25 <- get_AEZ(sites$env, sites$lat, sites$lon, buffer = 25000) aez25[, c("env", "aez_label", "majority_share")] ## Raw codes, no labelling get_AEZ(sites$env, sites$lat, sites$lon, legend = NA) ## Not run: ## A different GAEZ layer you downloaded yourself get_AEZ(sites$env, sites$lat, sites$lon, file = "GAEZ_AEZ_scenario.tif") ## End(Not run)
Returns the 19 standard bioclimatic indices (BIO1-BIO19) for each site, from the 1970-2000 WorldClim 2.1 normals.
get_bioclim( env.id = NULL, lat = NULL, lon = NULL, vars = 1:19, resolution = c("2.5m", "5m", "10m", "30s"), dir.path = NULL, verbose = TRUE )get_bioclim( env.id = NULL, lat = NULL, lon = NULL, vars = 1:19, resolution = c("2.5m", "5m", "10m", "30s"), dir.path = NULL, verbose = TRUE )
env.id |
character. Environment identifiers. |
lat, lon
|
numeric. Site coordinates (decimal degrees). |
vars |
integer or character. Which indices, e.g. |
resolution |
character. |
dir.path |
character. Cache directory. |
verbose |
boolean. |
data.frame with env, LAT, LON and one column
per requested index. Units follow WorldClim: temperature indices in C,
precipitation in mm, BIO3/BIO4/BIO15 dimensionless or percent.
Bioclim variables are LONG-TERM CLIMATE NORMALS, not the weather of your
trial year. They describe the climate a site is drawn from, so they are
appropriate for characterising a TPE, clustering locations, or as static
site covariates. They are NOT a substitute for
get_weather() + env_phenology() when you want to explain
what happened in a particular season: a site's BIO1 is identical in a
drought year and a wet year. Mixing normals and realised weather in one
kernel without saying which is which is a common and serious error.
Fick, S.E. & Hijmans, R.J. (2017) WorldClim 2: new 1-km spatial resolution climate surfaces for global land areas. International Journal of Climatology 37(12), 4302-4315. doi:10.1002/joc.5086
O'Donnell, M.S. & Ignizio, D.A. (2012) Bioclimatic predictors for supporting ecological applications in the conterminous United States. U.S. Geological Survey Data Series 691.
sites <- data.frame(env = c("PIRA", "SETE"), lat = c(-22.7, -19.4), lon = c(-47.6, -44.2)) bc <- get_bioclim(env.id = sites$env, lat = sites$lat, lon = sites$lon) bc2 <- get_bioclim(sites$env, sites$lat, sites$lon, vars = c(1, 12))sites <- data.frame(env = c("PIRA", "SETE"), lat = c(-22.7, -19.4), lon = c(-47.6, -44.2)) bc <- get_bioclim(env.id = sites$env, lat = sites$lat, lon = sites$lon) bc2 <- get_bioclim(sites$env, sites$lat, sites$lon, vars = c(1, 12))
Retrieves downscaled CMIP6 monthly climate (WorldClim 2.1) for a set of
sites and converts it into a DAILY weather series compatible with
processWTH(), env_phenology() and water_balance(), by
applying the monthly change signal to an observed baseline series
(delta-change downscaling).
get_climate_scenario( env.id = NULL, lat = NULL, lon = NULL, baseline = NULL, model = "MPI-ESM1-2-HR", models = c("IPSL-CM6A-LR", "MPI-ESM1-2-HR", "MRI-ESM2-0", "UKESM1-0-LL"), scenario = "ssp245", period = "2041-2060", resolution = c("2.5m", "5m", "10m", "30s"), method = c("delta", "raw"), dir.path = NULL, baseline.id = "env", baseline.date = "YYYYMMDD", precip.scale = c("multiplicative", "additive"), verbose = TRUE )get_climate_scenario( env.id = NULL, lat = NULL, lon = NULL, baseline = NULL, model = "MPI-ESM1-2-HR", models = c("IPSL-CM6A-LR", "MPI-ESM1-2-HR", "MRI-ESM2-0", "UKESM1-0-LL"), scenario = "ssp245", period = "2041-2060", resolution = c("2.5m", "5m", "10m", "30s"), method = c("delta", "raw"), dir.path = NULL, baseline.id = "env", baseline.date = "YYYYMMDD", precip.scale = c("multiplicative", "additive"), verbose = TRUE )
env.id |
vector (character). Site identifiers. |
lat, lon
|
vector (numeric). Site coordinates. |
baseline |
data.frame. Observed daily weather for the same sites,
from |
model |
character. CMIP6 GCM name, e.g. |
models |
vector (character). Used when |
scenario |
character. |
period |
character. |
resolution |
character. WorldClim tile resolution: |
method |
character. |
dir.path |
character. Cache directory for downloaded rasters.
Defaults to |
baseline.id, baseline.date
|
character. Columns in |
precip.scale |
character. |
verbose |
boolean. Print progress. |
What delta-change does and does not do. The monthly mean change from the GCM is added to (temperature) or multiplied onto (precipitation) the observed daily series. The result therefore keeps the OBSERVED day-to-day sequence, wet-day frequency and spell structure, and shifts only the monthly means. This is the standard, defensible way to drive a crop model from monthly projections (Hawkins et al. 2013), but note the consequence: changes in variability, in dry-spell length, and in extreme-event frequency are NOT represented. A scenario that in reality brings the same total rain in fewer, heavier events will look identical to one that brings it evenly. Heat-stress day counts under delta-change are driven purely by the mean shift, so they are a lower bound.
If extremes matter to your question, you need daily GCM output and a quantile-mapping bias correction, not this function.
Model choice is not innocuous. Across CMIP6, equilibrium climate
sensitivity ranges roughly 1.8-5.6 K, and several high-sensitivity models
are now regarded as implausibly hot. A single-model result is one draw from
a wide distribution. Prefer model = "ensemble" and report the spread;
a projection whose sign flips across models is not a finding.
Network access. WorldClim is an external host. In restricted
environments the download will fail; pre-populate dir.path with the
GeoTIFFs and the function will use the cache.
For method = "delta", a data.frame in get_weather() layout
(env, YYYYMMDD, daysFromStart, T2M,
T2M_MAX, T2M_MIN, PRECTOT, ...) with the scenario
signal applied, plus scenario, model and period
columns. For method = "raw", a monthly data.frame of projected
tmin, tmax and prec per site.
The attribute "deltas" holds the monthly change factors that were
applied, so the perturbation is auditable.
Germano Costa Neto
Fick, S.E. & Hijmans, R.J. (2017) WorldClim 2. Int. J. Climatol. 37(12), 4302-4315.
Hawkins, E. et al. (2013) Calibration and bias correction of climate projections for crop modelling. Agric. For. Meteorol. 170, 19-31.
O'Neill, B.C. et al. (2016) The Scenario Model Intercomparison Project (ScenarioMIP) for CMIP6. Geosci. Model Dev. 9, 3461-3482.
get_weather, project_risk,
env_phenology
sites <- data.frame(env = c("PIRA", "SETE"), lat = c(-22.7, -19.4), lon = c(-47.6, -44.2)) base <- get_weather(env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "2015-10-01", end.day = "2016-03-31") ## Mid-century, intermediate scenario, 4-model ensemble. ## The default 'models' set covers all four SSPs. fut <- get_climate_scenario(env.id = sites$env, lat = sites$lat, lon = sites$lon, baseline = base, model = "ensemble", scenario = "ssp245", period = "2041-2060") ## GFDL-ESM4 is fine for ssp126 or ssp370, just not ssp245/ssp585: fut126 <- get_climate_scenario(env.id = sites$env, lat = sites$lat, lon = sites$lon, baseline = base, model = "GFDL-ESM4", scenario = "ssp126", period = "2041-2060") ## Drop straight into the existing pipeline fut <- processWTH(fut) ph <- env_phenology(fut, crop = "maize") ## Compare thermal time now vs mid-century tapply(ph$dTT, ph$env, sum)sites <- data.frame(env = c("PIRA", "SETE"), lat = c(-22.7, -19.4), lon = c(-47.6, -44.2)) base <- get_weather(env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "2015-10-01", end.day = "2016-03-31") ## Mid-century, intermediate scenario, 4-model ensemble. ## The default 'models' set covers all four SSPs. fut <- get_climate_scenario(env.id = sites$env, lat = sites$lat, lon = sites$lon, baseline = base, model = "ensemble", scenario = "ssp245", period = "2041-2060") ## GFDL-ESM4 is fine for ssp126 or ssp370, just not ssp245/ssp585: fut126 <- get_climate_scenario(env.id = sites$env, lat = sites$lat, lon = sites$lon, baseline = base, model = "GFDL-ESM4", scenario = "ssp126", period = "2041-2060") ## Drop straight into the existing pipeline fut <- processWTH(fut) ph <- env_phenology(fut, crop = "maize") ## Compare thermal time now vs mid-century tapply(ph$dTT, ph$env, sum)
Returns elevation (m) for each site. Elevation is a strong covariate for temperature and radiation, so it is often worth carrying alongside the weather-derived envirotypes – but note it is STATIC: it adds between-site structure, never within-season dynamics.
get_elevation( env.id = NULL, lat = NULL, lon = NULL, resolution = c("2.5m", "5m", "10m", "30s"), dir.path = NULL, verbose = TRUE )get_elevation( env.id = NULL, lat = NULL, lon = NULL, resolution = c("2.5m", "5m", "10m", "30s"), dir.path = NULL, verbose = TRUE )
env.id |
character. Environment identifiers. |
lat, lon
|
numeric. Site coordinates (decimal degrees). |
resolution |
character. |
dir.path |
character. Cache directory. Default is the same cache used
by |
verbose |
boolean. |
Sites in the sea or outside the land mask return NA with a warning
naming them, rather than silently propagating NA into downstream kernels.
data.frame with env, LAT, LON,
ALT (m).
Fick, S.E. & Hijmans, R.J. (2017) WorldClim 2: new 1-km spatial resolution climate surfaces for global land areas. International Journal of Climatology 37(12), 4302-4315. doi:10.1002/joc.5086
sites <- data.frame(env = c("PIRA", "SETE"), lat = c(-22.7, -19.4), lon = c(-47.6, -44.2)) alt <- get_elevation(env.id = sites$env, lat = sites$lat, lon = sites$lon)sites <- data.frame(env = c("PIRA", "SETE"), lat = c(-22.7, -19.4), lon = c(-47.6, -44.2)) alt <- get_elevation(env.id = sites$env, lat = sites$lat, lon = sites$lon)
Builds the genomic, envirotypic and genotype-by-environment kernels required
by kernel_model. Given a genomic relationship matrix
(p x p genotypes) and optionally an environmental relationship matrix
(q x q environments), it returns one kernel per model term, already
spectrally decomposed and ready to sample from.
Two computational engines are available and produce numerically identical results:
engine = "factored" (default)Never allocates the
n x n kernel. Each kernel is represented by its eigenvectors
U (n x r) and eigenvalues s, obtained from the small
p x p / q x q inputs. Memory is O(n*r) instead of O(n^2),
which is what makes large multi-environment trials feasible.
engine = "dense"Builds the explicit n x n matrix and
attaches it as $Kernel. Slower and quadratic in memory, but needed
if you want to inspect or post-process the kernel itself.
Set verbose = TRUE to trace each step with timings;
verbose = FALSE (default) is entirely silent.
get_kernel( K_G = NULL, K_E = NULL, data = NULL, model = "RNMM", intercept.random = FALSE, engine = c("factored", "dense"), env = "env", gid = "gid", y = "value", tol = 1e-10, keep_var = 1, scale_kernels = TRUE, verbose = FALSE )get_kernel( K_G = NULL, K_E = NULL, data = NULL, model = "RNMM", intercept.random = FALSE, engine = c("factored", "dense"), env = "env", gid = "gid", y = "value", tol = 1e-10, keep_var = 1, scale_kernels = TRUE, verbose = FALSE )
K_G |
list of genomic relationship matrices ( |
K_E |
list of environmental relationship matrices ( |
data |
data.frame with the environment, genotype and phenotype columns. |
model |
character. One of |
intercept.random |
logical. Append an identity genotype kernel
|
engine |
character. |
env, gid, y
|
character. Column names in |
tol |
numeric. Eigenvalue cutoff, relative to the largest eigenvalue.
Must match |
keep_var |
numeric in (0, 1]. Retain only the leading eigenvalues
carrying this proportion of each kernel's trace. Sampler cost scales with
total rank, so e.g. |
scale_kernels |
logical. Normalise each kernel to
|
verbose |
logical. Narrate each step with timings. Default |
A named list of kernels, class "kernel_set". Each element is a
list with:
Type"D" (dense) or "BD" (block-diagonal).
decomplist of blocks, each
list(s, U, nr, deltav, type, pos, scaled, tol).
diag_meanmean(diag(K)), computed in O(n*r).
Kernelthe n x n matrix – only when
engine = "dense".
Attributes: "ne" (observations per environment), "kd_meta"
(decomposition settings, so kernel_model() can validate the cache),
and "obs" (index vectors and level names).
| model | terms | kernels |
MM |
y = mu + G | KG_G |
MDs |
y = mu + G + GE | KG_G, KGE_GE |
EMM |
y = mu + E + G | KE_W, KG_G |
EMDs |
y = mu + E + G + GE | KE_W, KG_G, KGE_GE |
RNMM |
y = mu + E + G + GxW | KE_W, KG_G, KGE_GW |
RNMDs |
y = mu + E + G + GE + GxW | KE_W, KG_G, KGE_GE, KGE_GW |
GE is a block-diagonal (within-environment) interaction; GxW is
the reaction-norm interaction built from the environmental kernel.
Every kernel here is a congruence transform of a small matrix. With
ig mapping observations to genotypes and ie to environments,
so the eigendecomposition of the n x n kernel follows from the small
one in O(p^3) rather than O(n^3). Writing with
, the nonzero eigenpairs come from the small Gram
matrix , giving an orthonormal U and exact eigenvalues.
The interaction kernel is an elementwise product of two expansions, which is
the expansion of a Kronecker product: with ,
. On a complete balanced grid its
eigenpairs are closed-form (eigenvalues , eigenvectors a
Khatri-Rao product) so no large eigendecomposition is ever required.
Costa-Neto et al (2021)
EnvRtype v.0.1.3, Sep 2026
Kernels depend only on genotypes and environments, never on the phenotype.
Masking y for a cross-validation fold does not invalidate
them, so build once and reuse across folds, chains and models.
Germano Costa Neto. Refactored version.
Costa-Neto G, Galli G, Carvalho HF, Crossa J, Fritsche-Neto R (2021). EnvRtype: a software to interplay enviromics and quantitative genomics in agriculture. G3, 11(4), jkab040.
kernel_model, decompose_kernels
data("maizeYield"); data("maizeG"); data("maizeWTH") ECs <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("FRUE", "PETP", "SRAD", "T2M_MAX"), statistic = "mean") KE <- list(W = env_kernel(env.data = ECs)[[2]]) KG <- list(G = maizeG) ## factored engine (default): no n x n matrix is ever allocated K <- get_kernel(K_G = KG, K_E = KE, data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value", verbose = TRUE) fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000, verbose = TRUE) ## trim the eigenvalue tail: usually a large speedup, tiny accuracy cost K99 <- get_kernel(K_G = KG, K_E = KE, data = maizeYield, model = "RNMM", keep_var = 0.99) ## separate marker-predictable from residual genetic variance Ki <- get_kernel(K_G = KG, K_E = KE, data = maizeYield, model = "RNMM", intercept.random = TRUE)data("maizeYield"); data("maizeG"); data("maizeWTH") ECs <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("FRUE", "PETP", "SRAD", "T2M_MAX"), statistic = "mean") KE <- list(W = env_kernel(env.data = ECs)[[2]]) KG <- list(G = maizeG) ## factored engine (default): no n x n matrix is ever allocated K <- get_kernel(K_G = KG, K_E = KE, data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value", verbose = TRUE) fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000, verbose = TRUE) ## trim the eigenvalue tail: usually a large speedup, tiny accuracy cost K99 <- get_kernel(K_G = KG, K_E = KE, data = maizeYield, model = "RNMM", keep_var = 0.99) ## separate marker-predictable from residual genetic variance Ki <- get_kernel(K_G = KG, K_E = KE, data = maizeYield, model = "RNMM", intercept.random = TRUE)
Queries the ISRIC SoilGrids REST API for soil properties at given coordinates.
get_soil( env.id = NULL, lat = NULL, lon = NULL, variables.names = "clay", depths = c("0-5cm", "5-15cm", "15-30cm", "30-60cm", "60-100cm", "100-200cm"), stat = c("mean", "q_5", "q_50", "q_95", "uncertainty"), wide = TRUE, sleep = 12, tries = 3L, timeout = 60, verbose = TRUE )get_soil( env.id = NULL, lat = NULL, lon = NULL, variables.names = "clay", depths = c("0-5cm", "5-15cm", "15-30cm", "30-60cm", "60-100cm", "100-200cm"), stat = c("mean", "q_5", "q_50", "q_95", "uncertainty"), wide = TRUE, sleep = 12, tries = 3L, timeout = 60, verbose = TRUE )
env.id |
vector (character). Environment identifiers. |
lat, lon
|
vector (numeric). Coordinates in decimal degrees (WGS84). |
variables.names |
vector (character). SoilGrids properties. See Details. |
depths |
vector (character). Depth intervals to request. |
stat |
character. Which summary to return: |
wide |
boolean. Return one row per environment with one column per property-depth (default), else long format. |
sleep |
numeric. Minimum seconds between successive API requests. Minimum 12 (SoilGrids fair-use limit of 5 requests/min). Network time counts toward this interval, and duplicate coordinates are fetched only once. |
tries |
integer. Retry attempts per failed request. |
timeout |
numeric. Maximum seconds to wait for a single SoilGrids response before the request is aborted and retried. Prevents one stalled connection from blocking the whole sequential download. Default 60. |
verbose |
boolean. Print progress messages. |
Accepted properties: bdod, cec, cfvo, clay,
nitrogen, ocd, ocs, phh2o, sand,
silt, soc, wv0010, wv0033, wv1500.
SoilGrids returns values in mapped units (e.g. clay in g/kg x 10). No unit
conversion is applied; the unit column records what was served.
A data.frame of soil features.
Brandon Monier, modified by Germano Costa Neto
Poggio, L. et al. (2021). SoilGrids 2.0: producing soil information for the globe with quantified spatial uncertainty. SOIL 7, 217-240.
get_weather, get_spatial
## Not run: ## Requires the live SoilGrids service. get_soil(env.id = "NM", lat = -13.05, lon = -56.05, variables.names = c("clay", "nitrogen")) ## End(Not run)## Not run: ## Requires the live SoilGrids service. get_soil(env.id = "NM", lat = -13.05, lon = -56.05, variables.names = c("clay", "nitrogen")) ## End(Not run)
Downloads SoilGrids properties one environment at a time with the same
checkpointing scheme as get_weather_resumable: each result is
written to disk immediately and recorded in an append-only log, so a
crashed run resumes with restart_from_log.
SoilGrids is rate-limited and slower than NASA POWER, which makes long soil jobs the most frequent victims of a mid-run failure.
get_soil_resumable( env.id = NULL, lat = NULL, lon = NULL, variables.names = "clay", depths = c("0-5cm", "5-15cm", "15-30cm", "30-60cm", "60-100cm", "100-200cm"), stat = c("mean", "q_5", "q_50", "q_95", "uncertainty"), dir.path = NULL, run.id = "soil_run", sleep = 20, tries = 3L, resume = TRUE, verbose = TRUE )get_soil_resumable( env.id = NULL, lat = NULL, lon = NULL, variables.names = "clay", depths = c("0-5cm", "5-15cm", "15-30cm", "30-60cm", "60-100cm", "100-200cm"), stat = c("mean", "q_5", "q_50", "q_95", "uncertainty"), dir.path = NULL, run.id = "soil_run", sleep = 20, tries = 3L, resume = TRUE, verbose = TRUE )
env.id, lat, lon, variables.names, depths, stat
|
Passed to
|
dir.path |
character. Parent directory. Default |
run.id |
character. Run directory name. Default |
sleep |
numeric. Seconds between environments. Default 20, matching
|
tries |
integer. Retry attempts per environment. Default 3. |
resume |
logical. Skip environments already logged |
verbose |
logical. Print progress. Default |
Results are stored long on disk, one row per property-depth, and reshaped to wide only at assembly. Storing long is what makes resumption safe: parts fetched at different times still stack correctly, whereas pre-widened parts could have mismatched columns.
See get_weather_resumable for the full checkpoint design.
A data.frame of soil properties (wide by default), with attributes
run.dir, n_ok, n_failed and failed.
restart_from_log, read_progress_log,
get_soil
## Not run: ## Long-running download job requiring access to SoilGrids. grid <- data.frame(env = paste0("P", 1:200), lat = runif(200, -13, -12), lon = runif(200, -56, -55)) soil <- get_soil_resumable( env.id = grid$env, lat = grid$lat, lon = grid$lon, variables.names = c("clay", "sand", "silt", "soc", "phh2o"), run.id = "zoning_grid") ## After a crash, in a new session: soil <- restart_from_log(run.id = "zoning_grid") ## Points in the ocean never resolve -- keep the successes soil <- restart_from_log(run.id = "zoning_grid", retry.failed = FALSE) ## End(Not run)## Not run: ## Long-running download job requiring access to SoilGrids. grid <- data.frame(env = paste0("P", 1:200), lat = runif(200, -13, -12), lon = runif(200, -56, -55)) soil <- get_soil_resumable( env.id = grid$env, lat = grid$lat, lon = grid$lon, variables.names = c("clay", "sand", "silt", "soc", "phh2o"), run.id = "zoning_grid") ## After a crash, in a new session: soil <- restart_from_log(run.id = "zoning_grid") ## Points in the ocean never resolve -- keep the successes soil <- restart_from_log(run.id = "zoning_grid", retry.failed = FALSE) ## End(Not run)
Extracts point values from a raster at the coordinates of each environment. This
function supersedes extract_GIS(), which relied on the retired raster
and sp packages.
get_spatial( digital.raster = NULL, which.raster.number = NULL, env.dataframe = NULL, env.id = NULL, lat = NULL, lng = NULL, name.feature = NULL, merge = TRUE, crs = 4326, verbose = TRUE )get_spatial( digital.raster = NULL, which.raster.number = NULL, env.dataframe = NULL, env.id = NULL, lat = NULL, lng = NULL, name.feature = NULL, merge = TRUE, crs = 4326, verbose = TRUE )
digital.raster |
SpatRaster, or a file path readable by |
which.raster.number |
integer. Layer(s) to keep when the input is multi-layer. |
env.dataframe |
data.frame containing the environment id and coordinates. |
env.id |
character. Name of the environment-id column in |
lat, lng
|
character. Names of the latitude/longitude columns, or numeric vectors of coordinates used directly. |
name.feature |
character. Name(s) for the extracted layer(s). |
merge |
boolean. Join the extracted values back onto |
crs |
numeric or character. CRS of the input coordinates. Default 4326 (WGS84). |
verbose |
boolean. Print progress messages. |
Coordinates are validated before extraction: latitude must lie in [-90, 90] and
longitude in [-180, 180]. This catches the common lat/lon transposition, which
otherwise yields silent NAs or values from the wrong hemisphere.
A data.frame of extracted values, joined to env.dataframe when
merge = TRUE.
Germano Costa Neto
get_weather, get_soil
if (requireNamespace("terra", quietly = TRUE) && requireNamespace("sf", quietly = TRUE)) { ## A small synthetic elevation raster covering the example site. elevation <- terra::rast(nrows = 2, ncols = 2, xmin = -57, xmax = -55, ymin = -14, ymax = -12, crs = "EPSG:4326") terra::values(elevation) <- c(100, 200, 300, 400) ref <- data.frame(env = "NM", lat = -13.05, lng = -56.05) get_spatial(env.dataframe = ref, digital.raster = elevation, lat = "lat", lng = "lng", env.id = "env", name.feature = "Elevation") }if (requireNamespace("terra", quietly = TRUE) && requireNamespace("sf", quietly = TRUE)) { ## A small synthetic elevation raster covering the example site. elevation <- terra::rast(nrows = 2, ncols = 2, xmin = -57, xmax = -55, ymin = -14, ymax = -12, crs = "EPSG:4326") terra::values(elevation) <- c(100, 200, 300, 400) ref <- data.frame(env = "NM", lat = -13.05, lng = -56.05) get_spatial(env.dataframe = ref, digital.raster = elevation, lat = "lat", lng = "lng", env.id = "env", name.feature = "Elevation") }
Imports daily weather data from the NASA POWER API and augments it with derived agro-meteorological indices (VPD, photoperiod, extraterrestrial radiation, temperature-humidity indices).
get_weather( env.id = NULL, lat = NULL, lon = NULL, start.day = NULL, end.day = NULL, variables.names = NULL, dir.path = NULL, save = FALSE, parallel = FALSE, workers = NULL, chunk_size = 29, sleep = 60, tries = 3L, verbose = TRUE )get_weather( env.id = NULL, lat = NULL, lon = NULL, start.day = NULL, end.day = NULL, variables.names = NULL, dir.path = NULL, save = FALSE, parallel = FALSE, workers = NULL, chunk_size = 29, sleep = 60, tries = 3L, verbose = TRUE )
env.id |
vector (character). Identifier of the site/environment. If
|
lat |
vector (numeric). Latitude in decimal degrees (WGS84). |
lon |
vector (numeric). Longitude in decimal degrees (WGS84). |
start.day, end.day
|
vector (character or Date). First and last date of
collection, e.g. |
variables.names |
vector (character). POWER parameters to request. See Details. |
dir.path |
character. Output directory when |
save |
boolean. Write one .csv per environment. |
parallel |
boolean. Download chunks in parallel. Default |
workers |
integer. Number of parallel processes. Defaults to 90% of
available cores, capped at |
chunk_size |
integer. Points per chunk. Default 29; raising this may exceed the API rate limit. |
sleep |
numeric. Seconds to pause between chunks. Default 60. |
tries |
integer. Retry attempts per failed request. Default 3. |
verbose |
boolean. Print progress messages. |
daysFromStart is date-based. It is computed as
as.numeric(YYYYMMDD - start.day) + 1, not as the row position. The previous
implementation used 1:nrow(), which silently mis-numbered every row after a
gap whenever the API returned fewer days than requested – assigning observations
to the wrong phenological stage in summaryWTH.
Missing values. POWER fill values (-999 and -99) are converted to
NA before any derived variable is computed.
Commonly used parameters include T2M, T2M_MAX, T2M_MIN,
T2MDEW, PRECTOTCORR (returned as PRECTOT), RH2M,
WS2M, ALLSKY_SFC_SW_DWN, ALLSKY_SFC_SW_DNI,
ALLSKY_SFC_PAR_TOT, GWETROOT, GWETTOP, EVPTRNS.
Derived variables, added when their inputs are present:
VPD: vapour pressure deficit (kPa), from T2MDEW, T2M_MAX, T2M_MIN
N: photoperiod (h), Forsythe et al. (1995)
RTA: extraterrestrial radiation (MJ/m2/day), FAO-56
n: actual sunshine duration (h)
TH1, TH2: temperature-humidity indices (NRC 1971; Yousef 1985)
PAR_TEMP: ratio of PAR to mean temperature
A data.frame with one row per environment-day. Always contains env,
LON, LAT, YYYYMMDD, DOY and daysFromStart,
followed by the requested and derived variables.
Germano Costa Neto and Giovanni Galli, modified by Tiago Olivoto
Sparks, A. (2018). nasapower: NASA-POWER Data from R. JOSS 3(30), 1035.
Forsythe, W.C., Rykiel, E.J., Stahl, R.S., Wu, H., Schoolfield, R.M. (1995). A model comparison for daylength as a function of latitude and day of the year. Ecological Modelling 80, 87-95.
summaryWTH, processWTH, get_soil,
get_weather_hourly
## Single location get_weather(env.id = "NM", lat = -13.05, lon = -56.05, start.day = "2015-02-15", end.day = "2015-06-15", variables.names = c("T2M", "PRECTOTCORR")) ## Two locations with different planting dates get_weather(env.id = c("NM", "SO"), lat = c(-13.05, -12.32), lon = c(-56.05, -55.42), start.day = c("2015-02-15", "2015-02-13"), end.day = rep("2015-06-15", 2))## Single location get_weather(env.id = "NM", lat = -13.05, lon = -56.05, start.day = "2015-02-15", end.day = "2015-06-15", variables.names = c("T2M", "PRECTOTCORR")) ## Two locations with different planting dates get_weather(env.id = c("NM", "SO"), lat = c(-13.05, -12.32), lon = c(-56.05, -55.42), start.day = c("2015-02-15", "2015-02-13"), end.day = rep("2015-06-15", 2))
Sibling collector to get_weather that downloads the NASA POWER
hourly endpoint, partitions each 24 h cycle into day / night /
pre-dawn, and returns one row per environment-day. It is a PARALLEL producer
of a covariate block; it does NOT feed processWTH() and must not be
wired into the daily pipeline by analogy.
get_weather_hourly( env.id, lat, lon, start.day, end.day, variables.names = NULL, night.def = c("solar", "fixed", "radiation"), night.hours = NULL, predawn.hours = 2:5, time_standard = c("LST", "UTC"), summarise = TRUE, thresholds = NULL, tries = 3, save = FALSE, dir.path = NULL, verbose = TRUE )get_weather_hourly( env.id, lat, lon, start.day, end.day, variables.names = NULL, night.def = c("solar", "fixed", "radiation"), night.hours = NULL, predawn.hours = 2:5, time_standard = c("LST", "UTC"), summarise = TRUE, thresholds = NULL, tries = 3, save = FALSE, dir.path = NULL, verbose = TRUE )
env.id |
character. Environment identifiers. |
lat, lon
|
numeric. Coordinates, recycled if length 1. |
start.day, end.day
|
character/Date. Collection window, recycled if length 1. |
variables.names |
character. POWER parameters. Maximum 15 on the hourly endpoint; validated before the request is sent. |
night.def |
character. How to define night:
|
night.hours |
integer. Hours 0-23 for |
predawn.hours |
integer. Window for pre-dawn statistics. Default 2:5. |
time_standard |
character. |
summarise |
boolean. |
thresholds |
named numeric. Night-hour counts above each threshold,
e.g. |
tries |
integer. Retry attempts per site. |
save |
boolean. Write one per-site CSV. |
dir.path |
character. Output directory when |
verbose |
boolean. Print progress messages. |
POWER hourly is MERRA-2 reanalysis, not observation. The diurnal cycle at 0.5 x 0.625 degrees is modelled and sub-daily fidelity is weaker than the daily aggregate.
A daily T2M_MIN is not a substitute for a night mean: the two differ
materially, and on a Q10 ~ 2 process a few degrees is a large
difference in implied rate (Peng et al. 2004).
A data.frame of class c("weather_hourly", "data.frame"). With
summarise = TRUE, one row per environment-day, carrying day/night/
pre-dawn statistics per variable. Radiation-type parameters
(shortwave, longwave, PAR) are summarised for DAYTIME only: they are ~0 at
night by construction, so night statistics would add no information.
Peng, S. et al. (2004) Rice yields decline with higher night temperature from global warming. PNAS 101(27), 9971-9975.
Forsythe, W.C. et al. (1995) A model comparison for daylength as a function of latitude and day of the year. Ecological Modelling 80, 87-95.
Allen, R.G. et al. (1998) Crop evapotranspiration. FAO Irrigation and Drainage Paper 56.
get_weather, processWTH,
summaryWTH
## Reuse the coordinates of a shipped daily dataset sites <- unique(maizeWTH[, c("env", "LON", "LAT")]) h <- get_weather_hourly(env.id = sites$env, lat = sites$LAT, lon = sites$LON, start.day = "2016-01-01", end.day = "2016-01-31", night.def = "solar") ## Night temperature is not the daily minimum with(h, cor(T2M_night_mean, T2M_day_min, use = "complete.obs")) ## Fixed night window, wrap-around handled correctly h2 <- get_weather_hourly(sites$env, sites$LAT, sites$LON, "2016-01-01", "2016-01-31", night.def = "fixed", night.hours = c(22:23, 0:5))## Reuse the coordinates of a shipped daily dataset sites <- unique(maizeWTH[, c("env", "LON", "LAT")]) h <- get_weather_hourly(env.id = sites$env, lat = sites$LAT, lon = sites$LON, start.day = "2016-01-01", end.day = "2016-01-31", night.def = "solar") ## Night temperature is not the daily minimum with(h, cor(T2M_night_mean, T2M_day_min, use = "complete.obs")) ## Fixed night window, wrap-around handled correctly h2 <- get_weather_hourly(sites$env, sites$LAT, sites$LON, "2016-01-01", "2016-01-31", night.def = "fixed", night.hours = c(22:23, 0:5))
Downloads daily weather one environment at a time, writing each result to
disk and appending a line to a progress log as soon as it succeeds. If the
session dies part-way through, restart_from_log resumes from
the last completed environment instead of restarting the whole job.
Use this instead of get_weather whenever losing the run
would hurt – in practice, more than a few dozen environments.
get_weather_resumable( env.id = NULL, lat = NULL, lon = NULL, start.day = NULL, end.day = NULL, variables.names = NULL, dir.path = NULL, run.id = "weather_run", sleep = 1, tries = 3L, resume = TRUE, verbose = TRUE )get_weather_resumable( env.id = NULL, lat = NULL, lon = NULL, start.day = NULL, end.day = NULL, variables.names = NULL, dir.path = NULL, run.id = "weather_run", sleep = 1, tries = 3L, resume = TRUE, verbose = TRUE )
env.id, lat, lon, start.day, end.day, variables.names
|
Passed to
|
dir.path |
character. Parent directory for run directories. Default
|
run.id |
character. Name of this run's directory. Default
|
sleep |
numeric. Seconds between environments. Default 1. |
tries |
integer. Retry attempts per environment before it is logged as failed. Default 3. |
resume |
logical. If |
verbose |
logical. Print progress. Default |
Why per-environment files. Results are written as one .rds
per environment under parts/, never accumulated only in memory. A
process killed at any point loses at most the environment in flight.
Why an append-only log. Each line is appended and flushed individually. Rewriting one state file every iteration would risk the whole file to a kill mid-write; appending risks only the last line, and the reader tolerates a truncated final record.
Why a fingerprint. The manifest stores a hash of coordinates, dates and variables. Resuming with different arguments is refused, because silently concatenating two different queries produces a table that looks fine and means nothing.
Failures do not abort the run. A failed environment is logged and
the loop continues. Inspect with read_progress_log, then
retry with restart_from_log.
Serial by design. Unlike get_weather, this wrapper
does not parallelise. Interleaved writes from several workers make progress
ordering ambiguous, and NASA POWER rate-limits aggressive clients anyway.
The cost is wall-clock time; the benefit is a log you can trust. For speed
on a job you can afford to lose, use get_weather(parallel = TRUE).
A data.frame of daily weather for every environment that succeeded,
with attributes run.dir, n_ok, n_failed and
failed.
restart_from_log, read_progress_log,
get_weather
## Not run: ## Long-running download job requiring access to NASA POWER. sites <- data.frame(env = paste0("E", 1:200), lat = runif(200, -30, -5), lon = runif(200, -60, -40)) wth <- get_weather_resumable( env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "2023-10-01", end.day = "2024-02-01", run.id = "trial2024") ## --- session dies at environment 137 --- ## In a NEW session, no arguments needed: wth <- restart_from_log(run.id = "trial2024") read_progress_log(run.id = "trial2024") attr(wth, "failed") ## Straight into the characterization pipeline wth <- processWTH(wth) W <- W_matrix(wth, env.id = "env", var.id = c("T2M", "PRECTOT")) ## End(Not run)## Not run: ## Long-running download job requiring access to NASA POWER. sites <- data.frame(env = paste0("E", 1:200), lat = runif(200, -30, -5), lon = runif(200, -60, -40)) wth <- get_weather_resumable( env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "2023-10-01", end.day = "2024-02-01", run.id = "trial2024") ## --- session dies at environment 137 --- ## In a NEW session, no arguments needed: wth <- restart_from_log(run.id = "trial2024") read_progress_log(run.id = "trial2024") attr(wth, "failed") ## Straight into the characterization pipeline wth <- processWTH(wth) W <- W_matrix(wth, env.id = "env", var.id = c("T2M", "PRECTOT")) ## End(Not run)
[scan_untested_envs()] allocates an 'ndraws x g x m' array, which is infeasible for raster-sized 'm'. This wrapper splits 'W_new' into chunks, scans each, and stitches the per-site results. Quantities that are defined ACROSS new sites – 'vcov' and 'cor' – cannot be recovered from chunks and are returned as 'NULL' with a warning if requested.
grid_scan( object, W_train, W_new, K_G = NULL, chunk_size = 500, max_draws = 200, verbose = TRUE, ... )grid_scan( object, W_train, W_new, K_G = NULL, chunk_size = 500, max_draws = 200, verbose = TRUE, ... )
object, W_train, K_G
|
as in [scan_untested_envs()]. |
W_new |
covariates for every grid cell / site (m x p). |
chunk_size |
sites per chunk. |
max_draws |
MCMC draws retained per chunk. Lower than the scan default because mapping needs the posterior mean far more than a tight interval. |
verbose |
report chunk progress. |
... |
passed to [scan_untested_envs()]. |
list with 'yHat_matrix', 'yHat_lower', 'yHat_upper', 'yHat_prob_win', 'genetic_var', 'interpolation', 'exceedance_detail', 'meta'. Class 'scan_untested_envs' so downstream functions accept it, but with 'vcov' and 'cor' set to 'NULL'.
In-sample yHat overstates accuracy. This masks a fold of phenotypes as NA, refits, and correlates held-out predictions against observed values.
kernel_cv( y, data, random, env, gid, folds = 5, scheme = c("cv1", "cv0"), seed = NULL, tol = 1e-10, scale_kernels = TRUE, ... )kernel_cv( y, data, random, env, gid, folds = 5, scheme = c("cv1", "cv0"), seed = NULL, tol = 1e-10, scale_kernels = TRUE, ... )
y |
character. Name of the response column in |
data |
data.frame with the response, environment and genotype columns. |
random |
named list of kernels passed to |
env |
character. Name of the environment column in |
gid |
character. Name of the genotype column in |
folds |
integer. Number of cross-validation folds. Default 5. |
scheme |
"cv1" (random cells) or "cv0" (leave-one-environment-out). |
seed |
integer or NULL. Optional seed for reproducible fold assignment. |
tol |
numeric. Eigenvalue tolerance passed to the kernel decomposition. |
scale_kernels |
logical. If |
... |
further arguments passed to |
The kernels are decomposed ONCE and reused across all folds: they depend only on genotypes and environments, never on the phenotype vector, so masking y does not invalidate them. This is the single largest saving in the package.
A list summarising out-of-sample predictive ability, with components
scheme (the validation scheme used), folds (number of folds),
accuracy (overall correlation between held-out predictions and
observations), rmse (overall root-mean-square error),
per_fold (a data.frame of per-fold accuracy and rmse),
fold_id (integer fold assignment for each row of data),
predicted (held-out predictions) and observed (the observed
response).
Fits Bayesian linear mixed models for multi-environment trials (MET) using
the kernels produced by get_kernel(). The model is
with and .
Sampling runs in the spectral basis of each kernel, so every kernel is used
through its eigendecomposition rather than as an n x n matrix.
The function returns genotype predictions, per-kernel variance components,
broad- and narrow-sense heritabilities, and – with
keep_effects = TRUE – posterior effect chains and breeder-facing
report tables (per-environment rankings, stability, selection candidates).
Set verbose = TRUE to trace the six execution steps with per-step and
total timings; verbose = FALSE (default) is entirely silent.
kernel_model( y, data = NULL, random = NULL, fixed = NULL, env, gid, verbose = FALSE, iterations = 1000, burnin = 200, thining = 10, tol = 1e-10, R2 = 0.5, digits = 4, seed = NULL, probs = c(0.025, 0.975), variance = c("realized", "scale"), scale_kernels = TRUE, keep_effects = FALSE, compute_CI = TRUE, R2_split = c("per_kernel", "shared"), prior_scale = c("eigen", "diag"), varcomp_method = c("postmean", "gibbs") )kernel_model( y, data = NULL, random = NULL, fixed = NULL, env, gid, verbose = FALSE, iterations = 1000, burnin = 200, thining = 10, tol = 1e-10, R2 = 0.5, digits = 4, seed = NULL, probs = c(0.025, 0.975), variance = c("realized", "scale"), scale_kernels = TRUE, keep_effects = FALSE, compute_CI = TRUE, R2_split = c("per_kernel", "shared"), prior_scale = c("eigen", "diag"), varcomp_method = c("postmean", "gibbs") )
y |
character. Name of the phenotype column in |
data |
data.frame with the environment, genotype and phenotype columns. Rows must be grouped by environment if any kernel is block-diagonal. |
random |
list of kernels from |
fixed |
matrix ( |
env, gid
|
character. Environment and genotype column names in |
verbose |
logical or 0/1. |
iterations |
integer. Total Gibbs iterations. Default |
burnin |
integer. Iterations discarded before collecting draws.
Default |
thining |
integer. Keep every |
tol |
numeric. Eigenvalue cutoff. Must match |
R2 |
numeric in (0, 1). Prior proportion of phenotypic variance explained
by the random terms. Default |
digits |
integer. Rounding for the variance-component tables. |
seed |
integer or |
probs |
length-2 numeric. Quantiles for posterior prediction intervals.
Default |
variance |
|
scale_kernels |
logical. Normalise each kernel to |
keep_effects |
logical. Retain per-draw effect chains. Required for
|
compute_CI |
logical. Compute posterior prediction intervals for
|
R2_split |
|
prior_scale |
|
varcomp_method |
|
Fits
with and
, by Gibbs sampling in the spectral basis of
each kernel. Writing , the effects are sampled as
with , so the cost
per iteration scales with the retained rank of each kernel, not with
. Trimming the eigenvalue tail via get_kernel(keep_var =) is
therefore the most direct way to speed up sampling.
variance = "scale" reports the raw kernel-scale parameter
sigb[j] (original BGGE behaviour, inflated for low-rank incidence
kernels such as KE_). variance = "realized" reports
var(u_j) averaged over draws – what most users mean by a variance
component. The raw scale is always returned as $sigb.
An object of class "kernel_model", a list with:
yHatnumeric vector of fitted/predicted values, length n.
yHat.CImatrix of posterior prediction intervals
(NULL unless compute_CI = TRUE).
varEposterior mean residual variance.
randomper-kernel posterior effects and variances.
fitthe full sampler object: chains, kernel names, MCMC
settings and – with keep_effects = TRUE – Uchain.
(Named $BGGE in versions before v1.2.3.)
metathe aligned env / gid / y data frame.
VarCompvariance-component table with confidence intervals.
varcompvalidated components and heritabilities, class
"km_varcomp"; see varcomp_summary.
varcomp_gibbsthe same computed by the Gibbs-chain estimator, for comparison.
runtimesampler wall time in seconds.
With keep_effects = TRUE it additionally carries
predictions, genetic_cor_env, genotype_effects,
env_summary and variance_proportions.
Costa-Neto et al (2021)
EnvRtype v.0.1.3, Sep 2026
Cached eigendecomposition. If random carries $decomp
(attached by get_kernel(decompose = TRUE)) and the cache matches
tol / scale_kernels / n, eigen() is skipped
entirely. Cross-validation folds and multiple chains reuse one decomposition.
No stored transposes. The original kept both U and
t(U) per kernel (doubling eigenvector memory) and computed
crossprod(tU, b). Now only U is stored and the back-transform
is U %*% b.
Optional chain storage. Full per-draw effect matrices
(k x nCum x n doubles) are allocated only when needed
(keep_effects = TRUE or yHat.CI). Otherwise running sums
reduce memory from O(k*nCum*n) to O(k*n).
Vectorised post-processing. Row-wise apply() over draw
matrices replaced by rowSums/rowMeans identities; sweep()
replaced by recycled arithmetic.
Scalar chains in a matrix rather than a nested list, removing per-iteration copy-on-modify of the chain container.
Two defects in the original parameterisation are addressed by arguments:
R2_split. The original gave every kernel the full
R2 share, so with nk kernels the priors collectively assert
nk * R2 of the phenotypic variance – with RNMDs (4 kernels,
R2 = 0.5) that is twice var(y). "per_kernel" (default)
uses R2 / nk so total prior mass stays at R2 regardless of model
size, keeping model comparisons on equal footing. "shared" restores the
original behaviour.
prior_scale. Kernels normalised to mean(diag(K)) = 1
are equal in scale but not in effective dimension: a rank-5 and a rank-750
kernel with the same mean diagonal carry very different total variance.
"eigen" (default) divides the prior scale by mean(s)
(= trace / rank), making priors comparable across kernels of differing rank.
Germano Costa Neto. Gibbs sampler adapted from Granato et al. (2018), BGGE package. Refactored version.
Costa-Neto G, Galli G, Carvalho HF, Crossa J, Fritsche-Neto R (2021). EnvRtype: a software to interplay enviromics and quantitative genomics in agriculture. G3, 11(4), jkab040.
Granato I, Cuevas J, Luna-Vazquez F, Crossa J, Montesinos-Lopez O, Burgueno J, Fritsche-Neto R (2018). BGGE: a new package for genomic-enabled prediction incorporating genotype x environment interaction models. G3, 8(9), 3039-3047.
get_kernel, varcomp_summary,
kernel_model_mc, kernel_cv
data("maizeYield"); data("maizeG"); data("maizeWTH") ECs <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("FRUE", "PETP", "SRAD", "T2M_MAX"), statistic = "mean") K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = ECs)[[2]]), data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value") ## silent fit fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000) fit fit$varcomp ## traced fit, with effect chains for the report tables fit2 <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000, keep_effects = TRUE, verbose = TRUE) head(fit2$genotype_effects) ## leave-one-environment-out: kernels do not depend on y, so build once for (e in unique(maizeYield$env)) { tr <- maizeYield; tr$value[tr$env == e] <- NA f <- kernel_model(y = "value", data = tr, random = K, env = "env", gid = "gid", iterations = 2000, burnin = 500) }data("maizeYield"); data("maizeG"); data("maizeWTH") ECs <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = c("FRUE", "PETP", "SRAD", "T2M_MAX"), statistic = "mean") K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = ECs)[[2]]), data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value") ## silent fit fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000) fit fit$varcomp ## traced fit, with effect chains for the report tables fit2 <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000, keep_effects = TRUE, verbose = TRUE) head(fit2$genotype_effects) ## leave-one-environment-out: kernels do not depend on y, so build once for (e in unique(maizeYield$env)) { tr <- maizeYield; tr$value[tr$env == e] <- NA f <- kernel_model(y = "value", data = tr, random = K, env = "env", gid = "gid", iterations = 2000, burnin = 500) }
"fixed" adds mega-environment means as fixed effects; "block" zeroes between-cluster entries of the GxE kernel so genotypes may rank differently across mega-environments; "both" does each.
kernel_model_clustered( y, data, random, env, gid, clusters, use = c("block", "fixed", "both"), gxe_pattern = "^KGE_|GE", fixed = NULL, ... )kernel_model_clustered( y, data, random, env, gid, clusters, use = c("block", "fixed", "both"), gxe_pattern = "^KGE_|GE", fixed = NULL, ... )
y |
character. Name of the response column in |
data |
data.frame with the response, environment and genotype columns. |
random |
named list of kernels passed to |
env |
character. Name of the environment column in |
gid |
character. Name of the genotype column in |
clusters |
an environment-cluster mapping: a |
use |
character. How clusters enter the model: |
gxe_pattern |
character. Regular expression selecting the GxE kernels to
block-diagonalise. Default |
fixed |
optional fixed-effects specification passed to |
... |
further arguments passed to |
Block-diagonalisation MODIFIES the GxE kernels, so any cached decomposition for those kernels is dropped and recomputed. Both dense and factored kernels are supported. Selected factored kernels are reconstructed as dense observation-by-observation matrices before blocking; unselected kernels are left unchanged.
An object of class "kernel_model" (see kernel_model)
with an added cluster_info element: a list recording the
environment-to-cluster mapping, the number of clusters k, the
mechanism used ("block", "fixed" or "both"), the
gxe_pattern, the number of added fixed columns n_fixed_cols
and the names of the blocked_kernels.
Runs independent chains with different seeds and reports R-hat and a crude ESS for the residual and each kernel-scale parameter. R-hat above 1.01 or a tiny ESS means the chain has not mixed: increase 'iterations'.
kernel_model_mc(..., n_chains = 3, seed = 1, tol = 1e-10, scale_kernels = TRUE)kernel_model_mc(..., n_chains = 3, seed = 1, tol = 1e-10, scale_kernels = TRUE)
... |
arguments passed to |
n_chains |
integer. Number of independent chains. Default 3. |
seed |
integer. Base seed; chain |
tol |
numeric. Eigenvalue tolerance passed to the kernel decomposition. |
scale_kernels |
logical. If |
The decomposition is shared across chains.
An object of class "kernel_model" (the fit of the first chain;
see kernel_model) with two added elements:
diagnostics, a data.frame giving the Gelman-Rubin Rhat and
effective sample size ESS for the residual variance and each
kernel-scale parameter, and all_chains, the list of individual
per-chain fits.
This data set was included in Souza et al. (2017) and Cuevas et al (2019) is from the Helix Seeds Company (HEL). Genotyping Array of 616 K SNPs (Single Nucleotide Polymorphism) (Unterseer et al. 2014). Standard quality controls (QC) were applied to the data, removing markers with a Call Rate $ 0.95. The remaining missing data in the lines were imputed with the Synbreed package (Wimmer et al. 2015) using the algorithms from the Beagle 4.0 software (Browning and Browning 2009). Markers with Minor Allele Frequency (MAF) of # 0.05 were removed. After applying QC, 52,811 SNPs were available to make the predictions. The phenotypic and genomic data of inbred lines are credited to Helix Seeds Ltda. Company.
data(maizeG)data(maizeG)
An object of class matrix (inherits from array) with 150 rows and 150 columns.
data(maizeG)data(maizeG)
Weather data collected from NASA POWER for each one of the five environments in HELIX dataset.
data(maizeWTH)data(maizeWTH)
A data.frame containing weather variables for each environment in HEL dataset
data(maizeWTH); head(maizeWTH)data(maizeWTH); head(maizeWTH)
This data set was included in Souza et al. (2017) and Cuevas et al (2019) is from the Helix Seeds Company (HEL). It consists of grain yied from 452 maize hybrids obtained by crossing 111 pure lines (inbreds); the hybrids were evaluated in 2015 at five Brazilian sites (E1-E5). The experimental design used in each site was a randomized block with two replicates per hybrid. However, to facilitate the demonstration of functions, only 150 hybrids per environment are being considered, thus counting 750 genotype x environment observations. Grain yield data are mean-centered and scaled.
data(maizeYield)data(maizeYield)
A data frame with 750 rows and 3 variables:
environmental id (factor)
genotypic id (factor)
grain yield value of genotype plus genotype by location interaction effects in kg ha-1 (numeric)
...
data(maizeYield); head(maizeYield)data(maizeYield); head(maizeYield)
Map scanned predictions, or training-network coverage
map_scan( scan, coords, lon_col = "lon", lat_col = "lat", date_col = NULL, type = c("performance", "coverage", "which_won_where"), genotypes = NULL, summarise = c("none", "mean"), facet_by = c("genotype", "date"), shp = NULL, clip = FALSE, mark_envelope = TRUE, envelope_level = c("gross", "mild"), win_threshold = 0.25, point_size = 2, palette = "viridis", output = c("plot", "table") )map_scan( scan, coords, lon_col = "lon", lat_col = "lat", date_col = NULL, type = c("performance", "coverage", "which_won_where"), genotypes = NULL, summarise = c("none", "mean"), facet_by = c("genotype", "date"), shp = NULL, clip = FALSE, mark_envelope = TRUE, envelope_level = c("gross", "mild"), win_threshold = 0.25, point_size = 2, palette = "viridis", output = c("plot", "table") )
scan |
a 'scan_untested_envs' / [grid_scan()] result. |
coords, lon_col, lat_col, date_col
|
as in [scan_spatial_table()]. |
type |
'"performance"' (predicted value), '"coverage"' (where the training network supports prediction), or '"which_won_where"' (the top genotype per site, and how many genotypes tie it). |
genotypes |
genotypes to map. Required for 'type = "performance"' unless 'summarise = "mean"'. For 'type = "which_won_where"' it restricts the pool of genotypes that compete for the win (default: all). |
summarise |
for 'type = "performance"': '"none"' (panel per genotype) or '"mean"' (single panel, mean over selected genotypes). |
facet_by |
'"genotype"' (A) or '"date"' (C). '"date"' needs 'date_col'. |
shp |
optional 'sf' polygon layer (e.g. a state or country border from 'geobr::read_state()') drawn underneath the points; also used to clip when 'clip = TRUE'. |
clip |
if 'TRUE' and 'shp' is supplied, keep only the scanned sites that fall inside 'shp' (points are matched to 'shp' in its CRS). |
mark_envelope |
hatch/grey cells outside the envelope. |
envelope_level |
'"gross"' (default) or '"mild"'. |
win_threshold |
for 'type = "which_won_where"': the minimum posterior probability of being the best genotype for another genotype to count as a co-winner at a site (default '0.25'). |
point_size, palette
|
cosmetic. |
output |
'"plot"' (default) returns a ggplot; '"table"' returns the data.frame that would have been plotted (one row per site, or per site-by-genotype for 'type = "performance"'), so you can build your own map. Clipping and 'summarise' are applied first, and a logical 'flagged' column marks sites outside the envelope. '"table"' needs no ggplot2. |
Three kinds of map are available through 'type':
'"performance"': the predicted value ('yHat') of one or more genotypes across the scanned sites, one panel per genotype (or a single panel of the mean with 'summarise = "mean"'). Sites outside the training covariate envelope are drawn as grey crosses rather than on the value scale.
'"coverage"': where the training network actually supports prediction, colouring each site by its geometric position relative to the training covariate cloud (interpolation, edge, mild- or gross-extrapolation).
'"which_won_where"': a classic which-won-where view for selection. It answers two questions with two maps (and two tables). Who won where is the single top genotype per site, by posterior mean ('yHat'; ties broken by the first genotype). Winners is how many genotypes are statistically tied for the win, taken from the scan's posterior draws: a genotype is a co-winner when its probability of being the best at that site ('prob_win') is at least 'win_threshold'. The count ('n_winners') and the tied genotypes ('winners', most probable first) are reported per site, so you can see where the decision is clear (one winner) versus where several genotypes are indistinguishable. This needs a scan from the current [scan_untested_envs()], which stores the per-site win probabilities.
a ggplot object, or a data.frame when 'output = "table"'. For 'type = "which_won_where"' a two-element list is returned instead: 'who_won_where' (the single top genotype per site, by posterior mean) and 'winners' (how many genotypes have posterior 'prob_win >= win_threshold'), each a ggplot, or a data.frame when 'output = "table"'.
## A scan is normally produced by scan_untested_envs(); here we build a ## minimal one by hand -- 2 genotypes predicted at 9 sites on a 3x3 grid. sites <- paste0("S", 1:9) grid <- expand.grid(lon = 1:3, lat = 1:3) yh <- rbind(G1 = 6 + grid$lon * 0.3 - grid$lat * 0.1, G2 = 5 + grid$lat * 0.4) colnames(yh) <- sites scan <- list( yHat_matrix = yh, yHat_lower = yh - 0.4, yHat_upper = yh + 0.4, ## posterior P(best) per genotype x site (illustrative softmax of yHat) yHat_prob_win = prop.table(exp(yh), margin = 2), interpolation = data.frame( env = sites, position = rep(c("interpolation", "edge", "gross_extrapolation"), each = 3), mahalanobis_ratio = seq(0.5, 3.5, length.out = 9), prop_outside = seq(0, 0.8, length.out = 9), weight_negativity = 0, stringsAsFactors = FALSE)) class(scan) <- "scan_untested_envs" ## Site coordinates, keyed by the scanned environment name coords <- data.frame(env = sites, lon = grid$lon, lat = grid$lat) if (requireNamespace("ggplot2", quietly = TRUE)) { ## Where does the training network support prediction? map_scan(scan, coords, type = "coverage") ## Predicted performance for a single genotype map_scan(scan, coords, type = "performance", genotypes = "G1") ## Mean over several genotypes in one panel map_scan(scan, coords, type = "performance", genotypes = c("G1", "G2"), summarise = "mean") ## Which genotype wins at each site, and how many genotypes tie it ww <- map_scan(scan, coords, type = "which_won_where") ww$who_won_where ww$winners } ## Export the plotted data instead of a figure, to map it your own way head(map_scan(scan, coords, type = "coverage", output = "table")) ## Both winner tables (no ggplot2 needed), one row per site ww_tab <- map_scan(scan, coords, type = "which_won_where", output = "table") head(ww_tab$who_won_where) head(ww_tab$winners) ## ---- Realistic scan: 50 new locations across Mato Grosso, Brazil -------- ## The bundled maize datasets are multi-environment trials from Mato Grosso: ## maizeWTH (daily weather), maizeYield (grain yield) and maizeG (genomic ## relationships). We train on them, then predict the trained genotypes at ## 50 candidate sites spread across the state. Needs BGGE (fitting) and a ## network connection (get_weather() queries NASA POWER). data("maizeWTH"); data("maizeYield"); data("maizeG") ## 1. Training environmental covariates (first 100 days after sowing) vars <- c("FRUE", "PETP", "GDD", "T2M_MAX") Wtr <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = vars, statistic = "mean") ## 2. Kernels + a fit that KEEPS the effect chains (required by any scan) K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = Wtr)[[2]]), data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value") fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000, keep_effects = TRUE) ## 3. Fifty candidate locations across Mato Grosso (WGS84 decimal degrees). ## The state spans roughly 7.3-18.0 S and 50.2-61.6 W. set.seed(2024) coords <- data.frame( env = sprintf("MT%02d", 1:50), lon = runif(50, -61.6, -50.2), lat = runif(50, -18.0, -7.3)) ## 4. Daily weather for each site over one summer season, processed into the ## SAME covariates as the training matrix. wth_new <- get_weather(env.id = coords$env, lat = coords$lat, lon = coords$lon, start.day = "2023-11-01", end.day = "2024-02-15", variables.names = c("T2M", "T2M_MAX", "T2M_MIN", "T2MDEW", "PRECTOT", "RH2M", "ALLSKY_SFC_SW_DWN")) wth_new <- processWTH(wth_new) # derives FRUE, PETP, GDD, ... Wnew <- W_matrix(env.data = wth_new[wth_new$daysFromStart < 100, ], var.id = vars, statistic = "mean") ## 5. Predict the trained genotypes at all 50 sites sc <- scan_untested_envs(fit, W_train = Wtr, W_new = Wnew, K_G = maizeG) ## 6. Coverage first: where does the trial network actually support prediction? map_scan(sc, coords, type = "coverage") ## Predicted performance of one genotype across Mato Grosso g1 <- rownames(maizeG)[1] map_scan(sc, coords, type = "performance", genotypes = g1) ## Mean predicted performance over the first ten genotypes map_scan(sc, coords, type = "performance", genotypes = rownames(maizeG)[1:10], summarise = "mean") ## 7. Overlay the Mato Grosso state border for context. geobr serves the ## official IBGE boundaries for Brazil as sf objects; clip = TRUE also ## drops any scanned site that falls outside the polygon. if (requireNamespace("geobr", quietly = TRUE) && requireNamespace("sf", quietly = TRUE)) { mt <- geobr::read_state(code_state = "MT", year = 2020) map_scan(sc, coords, type = "coverage", shp = mt) map_scan(sc, coords, type = "performance", genotypes = g1, shp = mt, clip = TRUE) }## A scan is normally produced by scan_untested_envs(); here we build a ## minimal one by hand -- 2 genotypes predicted at 9 sites on a 3x3 grid. sites <- paste0("S", 1:9) grid <- expand.grid(lon = 1:3, lat = 1:3) yh <- rbind(G1 = 6 + grid$lon * 0.3 - grid$lat * 0.1, G2 = 5 + grid$lat * 0.4) colnames(yh) <- sites scan <- list( yHat_matrix = yh, yHat_lower = yh - 0.4, yHat_upper = yh + 0.4, ## posterior P(best) per genotype x site (illustrative softmax of yHat) yHat_prob_win = prop.table(exp(yh), margin = 2), interpolation = data.frame( env = sites, position = rep(c("interpolation", "edge", "gross_extrapolation"), each = 3), mahalanobis_ratio = seq(0.5, 3.5, length.out = 9), prop_outside = seq(0, 0.8, length.out = 9), weight_negativity = 0, stringsAsFactors = FALSE)) class(scan) <- "scan_untested_envs" ## Site coordinates, keyed by the scanned environment name coords <- data.frame(env = sites, lon = grid$lon, lat = grid$lat) if (requireNamespace("ggplot2", quietly = TRUE)) { ## Where does the training network support prediction? map_scan(scan, coords, type = "coverage") ## Predicted performance for a single genotype map_scan(scan, coords, type = "performance", genotypes = "G1") ## Mean over several genotypes in one panel map_scan(scan, coords, type = "performance", genotypes = c("G1", "G2"), summarise = "mean") ## Which genotype wins at each site, and how many genotypes tie it ww <- map_scan(scan, coords, type = "which_won_where") ww$who_won_where ww$winners } ## Export the plotted data instead of a figure, to map it your own way head(map_scan(scan, coords, type = "coverage", output = "table")) ## Both winner tables (no ggplot2 needed), one row per site ww_tab <- map_scan(scan, coords, type = "which_won_where", output = "table") head(ww_tab$who_won_where) head(ww_tab$winners) ## ---- Realistic scan: 50 new locations across Mato Grosso, Brazil -------- ## The bundled maize datasets are multi-environment trials from Mato Grosso: ## maizeWTH (daily weather), maizeYield (grain yield) and maizeG (genomic ## relationships). We train on them, then predict the trained genotypes at ## 50 candidate sites spread across the state. Needs BGGE (fitting) and a ## network connection (get_weather() queries NASA POWER). data("maizeWTH"); data("maizeYield"); data("maizeG") ## 1. Training environmental covariates (first 100 days after sowing) vars <- c("FRUE", "PETP", "GDD", "T2M_MAX") Wtr <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = vars, statistic = "mean") ## 2. Kernels + a fit that KEEPS the effect chains (required by any scan) K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = Wtr)[[2]]), data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value") fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", iterations = 5000, burnin = 1000, keep_effects = TRUE) ## 3. Fifty candidate locations across Mato Grosso (WGS84 decimal degrees). ## The state spans roughly 7.3-18.0 S and 50.2-61.6 W. set.seed(2024) coords <- data.frame( env = sprintf("MT%02d", 1:50), lon = runif(50, -61.6, -50.2), lat = runif(50, -18.0, -7.3)) ## 4. Daily weather for each site over one summer season, processed into the ## SAME covariates as the training matrix. wth_new <- get_weather(env.id = coords$env, lat = coords$lat, lon = coords$lon, start.day = "2023-11-01", end.day = "2024-02-15", variables.names = c("T2M", "T2M_MAX", "T2M_MIN", "T2MDEW", "PRECTOT", "RH2M", "ALLSKY_SFC_SW_DWN")) wth_new <- processWTH(wth_new) # derives FRUE, PETP, GDD, ... Wnew <- W_matrix(env.data = wth_new[wth_new$daysFromStart < 100, ], var.id = vars, statistic = "mean") ## 5. Predict the trained genotypes at all 50 sites sc <- scan_untested_envs(fit, W_train = Wtr, W_new = Wnew, K_G = maizeG) ## 6. Coverage first: where does the trial network actually support prediction? map_scan(sc, coords, type = "coverage") ## Predicted performance of one genotype across Mato Grosso g1 <- rownames(maizeG)[1] map_scan(sc, coords, type = "performance", genotypes = g1) ## Mean predicted performance over the first ten genotypes map_scan(sc, coords, type = "performance", genotypes = rownames(maizeG)[1:10], summarise = "mean") ## 7. Overlay the Mato Grosso state border for context. geobr serves the ## official IBGE boundaries for Brazil as sf objects; clip = TRUE also ## drops any scanned site that falls outside the polygon. if (requireNamespace("geobr", quietly = TRUE) && requireNamespace("sf", quietly = TRUE)) { mt <- geobr::read_state(code_state = "MT", year = 2020) map_scan(sc, coords, type = "coverage", shp = mt) map_scan(sc, coords, type = "performance", genotypes = g1, shp = mt, clip = TRUE) }
Computes vapour pressure deficit, the slope of the saturation vapour pressure curve, Priestley-Taylor potential evapotranspiration and the precipitation balance, following FAO-56.
param_atmospheric( env.data, PREC = NULL, Tdew = NULL, Tmin = NULL, Tmax = NULL, RH = NULL, Rad = NULL, G = 0, Alt = 600, alpha = 1.26, merge = FALSE, verbose = TRUE )param_atmospheric( env.data, PREC = NULL, Tdew = NULL, Tmin = NULL, Tmax = NULL, RH = NULL, Rad = NULL, G = 0, Alt = 600, alpha = 1.26, merge = FALSE, verbose = TRUE )
env.data |
data.frame. A |
PREC, Tdew, Tmin, Tmax, RH, Rad
|
character. Column names for precipitation, dew point, minimum and maximum temperature, relative humidity and solar radiation. Defaults follow NASA POWER naming. |
G |
numeric. Soil heat flux (MJ/m2/day). Default 0. |
Alt |
numeric scalar, or character naming an elevation column in
|
alpha |
numeric. Priestley-Taylor coefficient. Default 1.26. |
merge |
boolean. Bind results onto |
verbose |
boolean. Print a summary of what was computed. |
Elevation now works. Alt accepts either a numeric elevation or the
name of a column. Previously the default Alt = 600 was never NULL,
so the elevation column was never read and every site was treated as 600 m –
a 26% error in atmospheric pressure at 2500 m, propagating into the
psychrometric constant and ETP.
VPD is the difference between mean saturation vapour pressure (from Tmin
and Tmax) and actual vapour pressure (from dew point). Values are floored at 0;
negatives can only arise from a dew point above air temperature, which is
unphysical and indicates a data problem. A warning is raised when this occurs.
A data.frame with VPD (kPa), SPV (kPa/C), ETP (mm/day) and
PETP (mm/day).
Germano Costa Neto
param_radiation, param_temperature,
processWTH
env.data <- get_weather(lat = -13.05, lon = -56.05) param_atmospheric(env.data, Alt = 850)env.data <- get_weather(lat = -13.05, lon = -56.05) param_atmospheric(env.data, Alt = 850)
Computes extraterrestrial radiation, daylength and actual sunshine duration from day of year and latitude, following FAO-56.
param_radiation( env.data, day.id = "DOY", latitude = "LAT", solarRadiation = NULL, merge = TRUE, verbose = TRUE )param_radiation( env.data, day.id = "DOY", latitude = "LAT", solarRadiation = NULL, merge = TRUE, verbose = TRUE )
env.data |
data.frame. A |
day.id |
character. Column holding day of year. Default |
latitude |
character. Column holding latitude. Default |
solarRadiation |
character. Column holding incoming shortwave radiation.
Default |
merge |
boolean. Bind results onto |
verbose |
boolean. Print a summary of what was computed. |
Daylength uses Forsythe et al. (1995), consistent with get_weather.
The two functions previously used different formulas that disagreed by up to
0.5 h at high latitude.
The sunset hour angle is clamped so polar day/night yields 0 rather than
NaN. Missing radiation is median-imputed within the supplied data, and
n is bounded to [0, N].
A data.frame with n (actual sunshine hours), N (daylength, h) and
RTA (extraterrestrial radiation, MJ/m2/day).
Germano Costa Neto
param_atmospheric, param_temperature,
processWTH
env.data <- get_weather(lat = -13.05, lon = -56.05) param_radiation(env.data)env.data <- get_weather(lat = -13.05, lon = -56.05) param_radiation(env.data)
Computes growing degree days, the temperature-limited radiation use efficiency factor, and daily temperature range.
param_temperature( env.data, Tmax = NULL, Tmin = NULL, Tbase1 = 9, Tbase2 = 45, Topt1 = 26, Topt2 = 32, method = c("standard", "legacy"), merge = FALSE, verbose = TRUE )param_temperature( env.data, Tmax = NULL, Tmin = NULL, Tbase1 = 9, Tbase2 = 45, Topt1 = 26, Topt2 = 32, method = c("standard", "legacy"), merge = FALSE, verbose = TRUE )
env.data |
data.frame. A |
Tmax, Tmin
|
character. Column names for maximum and minimum air temperature (Celsius). |
Tbase1 |
numeric. Base temperature below which development stops. |
Tbase2 |
numeric. Upper temperature at which development stops. |
Topt1, Topt2
|
numeric. Lower and upper bounds of the optimal range. |
method |
character. GDD formulation: |
merge |
boolean. Bind results onto |
verbose |
boolean. Print a summary of what was computed. |
GDD methods.
"standard".
Tmax is capped at the upper threshold; the result is floored at zero. This is
the conventional agronomic definition.
"legacy"Clamps both Tmax and Tmin into
before averaging. This is the behaviour of
earlier EnvRtype versions and corresponds to the "Method 2" variant. It
credits heat units on days colder than – e.g. with
Tmax 12, Tmin 5, Tbase1 9 it returns 1.5 where the standard method returns 0.
The default changed from legacy to standard; results will differ on cold days.
Pass method = "legacy" to reproduce earlier output.
FRUE rises linearly from 0 at Tbase1 to 1 at Topt1, holds at
1 across the optimum, and falls to 0 at Tbase2.
A data.frame with GDD (C/day), FRUE (0-1) and T2M_RANGE (C).
Germano Costa Neto
param_radiation, param_atmospheric,
processWTH
env.data <- get_weather(lat = -13.05, lon = -56.05) param_temperature(env.data) param_temperature(env.data, method = "legacy") # pre-refactor behaviourenv.data <- get_weather(lat = -13.05, lon = -56.05) param_temperature(env.data) param_temperature(env.data, method = "legacy") # pre-refactor behaviour
phenology_templates() returns the built-in crop templates as a
data.frame; show_phenology() prints them, optionally for one crop
with its stage table, provenance and caveats.
Every template is a published tabulation for a REFERENCE cultivar. They
give a consistent relative scale across sites – which is what fixes the
calendar-day misalignment in W_matrix() – but they are not
calibrated predictions for your germplasm. Maturity group alone shifts
maize R6 by 300-500 C d. Validate against observed flowering dates before
treating stage labels as phenology.
phenology_templates() show_phenology(crop = NULL)phenology_templates() show_phenology(crop = NULL)
crop |
character. Optional single crop; if omitted, all are listed. |
phenology_templates() a data.frame, one row per crop.
show_phenology() returns its input invisibly and is called for
the side effect of printing.
phenology_templates() show_phenology("safflower")phenology_templates() show_phenology("safflower")
Reshapes a 'scan_untested_envs()' result into one row per genotype x planting date, carrying the credible bounds and the envelope position. Useful on its own when you want the numbers rather than a plot.
planting_window_table( scan, dates, date_col = "date", genotypes = NULL, group = NULL )planting_window_table( scan, dates, date_col = "date", genotypes = NULL, group = NULL )
scan |
a 'scan_untested_envs' object whose new "environments" are candidate planting dates. |
dates |
either (a) a data.frame with a column ‘env' matching the scan’s new-environment names and a column giving the date, or (b) a vector of dates in the same order as the scan's new environments. |
date_col |
name of the date column when 'dates' is a data.frame. |
genotypes |
optional character vector to subset genotypes. |
group |
optional named vector or data.frame ('gid', 'group') assigning genotypes to groups, for group-mean curves. |
data.frame with 'gid', 'env', 'date', 'yHat', 'lower', 'upper', 'ci_width', 'position', 'mahalanobis_ratio', 'prop_outside', 'weight_negativity', and 'group' when supplied.
Curves of predicted performance against sowing date, with credible ribbons and out-of-envelope dates marked. Dates classified 'gross_extrapolation' are desaturated and shaded, and counted in the subtitle, because position is reported but must not be read as a reliability guarantee.
plot_planting_window( scan, dates = NULL, date_col = "date", genotypes = NULL, group = NULL, summarise = c("genotype", "group", "overall"), ribbon = TRUE, ribbon_max = 6, mark_envelope = TRUE, envelope_level = c("gross", "mild"), engine = c("ggplot2", "base"), ylab = "Predicted performance", title = NULL )plot_planting_window( scan, dates = NULL, date_col = "date", genotypes = NULL, group = NULL, summarise = c("genotype", "group", "overall"), ribbon = TRUE, ribbon_max = 6, mark_envelope = TRUE, envelope_level = c("gross", "mild"), engine = c("ggplot2", "base"), ylab = "Predicted performance", title = NULL )
scan |
a 'scan_untested_envs' object, or a table from [planting_window_table()]. |
dates, date_col, genotypes, group
|
passed to [planting_window_table()] when 'scan' is a scan object. |
summarise |
one of '"genotype"' (one curve per genotype), '"group"' (mean curve per group, requires 'group'), or '"overall"' (single mean curve across all selected genotypes). |
ribbon |
draw credible ribbons. Switched off automatically when more than 'ribbon_max' curves are drawn, since overlapping ribbons are unreadable. |
ribbon_max |
curve count above which ribbons are suppressed. |
mark_envelope |
shade and desaturate dates outside the envelope. |
envelope_level |
which positions to mark: '"gross"' (default) or '"mild"' (also marks 'mild_extrapolation'). |
engine |
'"ggplot2"' (default) or '"base"'. |
ylab, title
|
axis label and title. |
a ggplot object (invisibly for 'engine = "base"').
Scatter of the target against the realised (K-whitened) off-diagonal environment correlations, with the 1:1 line.
## S3 method for class 'sim_met' plot(x, ...)## S3 method for class 'sim_met' plot(x, ...)
x |
a |
... |
passed to |
x, invisibly.
Two panels: the realised kernel off-diagonals against the target environment correlation, and the scree of per-PC variance shares.
## S3 method for class 'sim_W' plot(x, ...)## S3 method for class 'sim_W' plot(x, ...)
x |
a |
... |
passed to |
x, invisibly.
Print a kernel set
## S3 method for class 'kernel_set' print(x, ...)## S3 method for class 'kernel_set' print(x, ...)
x |
object of class |
... |
ignored. |
Returns x invisibly. Called for its side effect of printing a
one-line header and a per-kernel table (kernel name, retained rank,
mean diagonal and rank as a percentage of the number of observations) to
the console.
Compact console summary of a scan_untested_envs result: the
per-site prediction and its extrapolation diagnostics, a tally of position
labels, the covariates that fell outside the training range, and a reminder
of how the geometric measures relate to calibrated accuracy.
## S3 method for class 'scan_untested_envs' print(x, ...)## S3 method for class 'scan_untested_envs' print(x, ...)
x |
an object of class |
... |
ignored; present for S3 compatibility. |
x, invisibly.
Print a Simulated Envirome Diversity Matrix
## S3 method for class 'sim_envirome' print(x, ...)## S3 method for class 'sim_envirome' print(x, ...)
x |
a |
... |
ignored. |
x, invisibly.
Print a Simulated Marker Matrix
## S3 method for class 'sim_markers' print(x, ...)## S3 method for class 'sim_markers' print(x, ...)
x |
a |
... |
ignored. |
x, invisibly.
Print a Simulated Multi-Environment Trial
## S3 method for class 'sim_met' print(x, ...)## S3 method for class 'sim_met' print(x, ...)
x |
a |
... |
ignored. |
x, invisibly.
Print a Simulated Envirome
## S3 method for class 'sim_W' print(x, ...)## S3 method for class 'sim_W' print(x, ...)
x |
a |
... |
ignored. |
x, invisibly.
Compact summary of a fitted soil mixture model: the selected model and cluster count, the BIC gap to the runner-up, cluster sizes, the number of uncertain sites, and the per-cluster profile means in original units.
The BIC gap is reported because a point estimate of the zone count is misleading on its own. Differences are graded weak (< 2), positive (2-6), strong (6-10) and very strong (> 10). A weak gap means several partitions fit about equally well.
## S3 method for class 'soil_gmm' print(x, ...)## S3 method for class 'soil_gmm' print(x, ...)
x |
an object of class |
... |
ignored. |
x, invisibly.
if (requireNamespace("mclust", quietly = TRUE)) { ## Synthetic topsoil organic carbon and pH in SoilGrids mapped units. set.seed(1) soil_grid <- data.frame(env = paste0("E", 1:60)) soil_grid[["soc|0_5cm"]] <- c(rnorm(30, 100, 10), rnorm(30, 300, 20)) soil_grid[["phh2o|0_5cm"]] <- c(rnorm(30, 50, 2), rnorm(30, 65, 2)) fit <- soil_classification(soil_grid, risk.vars = "soc", G = 2, model.names = "VVI") print(fit) }if (requireNamespace("mclust", quietly = TRUE)) { ## Synthetic topsoil organic carbon and pH in SoilGrids mapped units. set.seed(1) soil_grid <- data.frame(env = paste0("E", 1:60)) soil_grid[["soc|0_5cm"]] <- c(rnorm(30, 100, 10), rnorm(30, 300, 20)) soil_grid[["phh2o|0_5cm"]] <- c(rnorm(30, 50, 2), rnorm(30, 65, 2)) fit <- soil_classification(soil_grid, risk.vars = "soc", G = 2, model.names = "VVI") print(fit) }
Print an Envirotype (T) Matrix
## S3 method for class 'T_matrix' print(x, ...)## S3 method for class 'T_matrix' print(x, ...)
x |
a |
... |
ignored. |
x, invisibly.
Convenience wrapper chaining param_radiation,
param_atmospheric and param_temperature.
processWTH( env.data, PREC = NULL, Tdew = NULL, Tmin = NULL, Tmax = NULL, RH = NULL, Rad = NULL, G = 0, Alt = 600, Tbase1 = 9, Tbase2 = 45, Topt1 = 26, Topt2 = 32, method = c("standard", "legacy"), alpha = 1.26, verbose = TRUE )processWTH( env.data, PREC = NULL, Tdew = NULL, Tmin = NULL, Tmax = NULL, RH = NULL, Rad = NULL, G = 0, Alt = 600, Tbase1 = 9, Tbase2 = 45, Topt1 = 26, Topt2 = 32, method = c("standard", "legacy"), alpha = 1.26, verbose = TRUE )
env.data |
data.frame. A |
PREC, Tdew, Tmin, Tmax, RH, Rad
|
character. Column names passed through to the underlying functions. |
G |
numeric. Soil heat flux. Default 0. |
Alt |
numeric or character. Elevation in metres, or the name of an elevation column. |
Tbase1, Tbase2, Topt1, Topt2
|
numeric. Cardinal temperatures. |
method |
character. GDD method, see |
alpha |
numeric. Priestley-Taylor coefficient. |
verbose |
boolean. Print progress messages. |
If env.data already carries n, N or RTA from
get_weather, those columns are dropped before recomputation to avoid
duplicated names. Note that get_weather() derives n from
ALLSKY_SFC_SW_DNI whereas param_radiation() uses
ALLSKY_SFC_SW_DWN, so the recomputed values may differ.
The input data.frame with radiation, atmospheric and thermal parameters appended.
Germano Costa Neto
get_weather, summaryWTH
env.data <- get_weather(lat = -13.05, lon = -56.05) processWTH(env.data)env.data <- get_weather(lat = -13.05, lon = -56.05) processWTH(env.data)
Runs env_risk_profile() on an observed series and on a
delta-shifted scenario series, and returns the change in stress frequency
per site, stage and class.
project_risk(baseline, scenario, ..., verbose = TRUE)project_risk(baseline, scenario, ..., verbose = TRUE)
baseline |
data.frame. Observed multi-year daily weather. |
scenario |
data.frame. Output of |
... |
Arguments passed to |
verbose |
boolean. Print progress. |
Because get_climate_scenario(method = "delta") preserves the observed
day-to-day sequence, differences here reflect the MEAN shift only. Rising
frequencies of heat classes are real but conservative; unchanged
precipitation-class frequencies do not mean rainfall risk is unchanged, only
that the mean-preserving perturbation left wet-day counts alone.
A list of class "env_risk_delta" with baseline,
scenario (both "env_risk" objects) and change, a
data.frame with freq_base, freq_scen, freq_diff and
days_diff.
env_risk_profile, get_climate_scenario
## Illustrative mean warming, not a downloaded climate projection. wth <- expand.grid(day = 0:149, year = 2021:2023, env = c("E1", "E2")) wth$YYYYMMDD <- as.Date(paste0(wth$year, "-01-01")) + wth$day wth$T2M_MAX <- 28 + 6 * sin(wth$day / 20) fut <- wth fut$T2M_MAX <- fut$T2M_MAX + 3 chg <- project_risk(baseline = wth, scenario = fut, var.id = "T2M_MAX", by.stage = FALSE) subset(chg$change, class == "heat_severe")## Illustrative mean warming, not a downloaded climate projection. wth <- expand.grid(day = 0:149, year = 2021:2023, env = c("E1", "E2")) wth$YYYYMMDD <- as.Date(paste0(wth$year, "-01-01")) + wth$day wth$T2M_MAX <- 28 + 6 * sin(wth$day / 20) fut <- wth fut$T2M_MAX <- fut$T2M_MAX + 3 chg <- project_risk(baseline = wth, scenario = fut, var.id = "T2M_MAX", by.stage = FALSE) subset(chg$change, class == "heat_severe")
Returns the per-environment retrieval history written by
get_weather_resumable or get_soil_resumable:
what succeeded, what failed, when, and with which error message.
read_progress_log(run.id, dir.path = NULL, all = FALSE, verbose = TRUE)read_progress_log(run.id, dir.path = NULL, all = FALSE, verbose = TRUE)
run.id |
character. Run directory name. |
dir.path |
character. Parent directory. Default |
all |
logical. If |
verbose |
boolean. If |
A data.frame with env, key, status
("ok" or "failed"), time and message.
restart_from_log
## Not run: ## Requires a saved run created by get_soil_resumable(). lg <- read_progress_log(run.id = "zoning_grid") table(lg$status) ## Why did they fail? subset(lg, status == "failed")[, c("env", "message")] ## Full history including retries read_progress_log(run.id = "zoning_grid", all = TRUE) ## End(Not run)## Not run: ## Requires a saved run created by get_soil_resumable(). lg <- read_progress_log(run.id = "zoning_grid") table(lg$status) ## Why did they fail? subset(lg, status == "failed")[, c("env", "message")] ## Full history including retries read_progress_log(run.id = "zoning_grid", all = TRUE) ## End(Not run)
Reads the manifest and progress log written by
get_weather_resumable or get_soil_resumable,
skips every environment already retrieved, fetches those still missing or
failed, and returns the assembled result.
Safe to call repeatedly. If everything already succeeded it fetches nothing and simply assembles what is on disk.
restart_from_log( run.id, dir.path = NULL, retry.failed = TRUE, sleep = NULL, tries = NULL, verbose = TRUE )restart_from_log( run.id, dir.path = NULL, retry.failed = TRUE, sleep = NULL, tries = NULL, verbose = TRUE )
run.id |
character. The run directory name used originally. |
dir.path |
character. Parent directory. Default |
retry.failed |
logical. Retry environments previously logged as
failed. Default |
sleep |
numeric or |
tries |
integer or |
verbose |
logical. Print progress. Default |
All retrieval parameters come from manifest.json, so a resumed run
necessarily uses the same coordinates, dates and variables as the original.
You cannot accidentally resume with different settings – that is what the
stored fingerprint prevents.
Permanently failing environments. Some failures never resolve: a
point in the ocean has no SoilGrids profile. Repeated calls will retry them
every time. Once you have confirmed a failure is permanent, pass
retry.failed = FALSE.
A data.frame as returned by the original function, with attributes
run.dir, n_ok, n_failed and failed.
get_weather_resumable,
get_soil_resumable, read_progress_log
## Not run: ## A long soil job dies at environment 137 of 200. ## In a brand-new session -- no need to remember the arguments: soil <- restart_from_log(run.id = "zoning_grid") ## Inspect what failed and why lg <- read_progress_log(run.id = "zoning_grid") subset(lg, status == "failed")[, c("env", "message")] ## Give up on permanent failures soil <- restart_from_log(run.id = "zoning_grid", retry.failed = FALSE) ## Weather resumes identically wth <- restart_from_log(run.id = "trial2024") ## End(Not run)## Not run: ## A long soil job dies at environment 137 of 200. ## In a brand-new session -- no need to remember the arguments: soil <- restart_from_log(run.id = "zoning_grid") ## Inspect what failed and why lg <- read_progress_log(run.id = "zoning_grid") subset(lg, status == "failed")[, c("env", "message")] ## Give up on permanent failures soil <- restart_from_log(run.id = "zoning_grid", retry.failed = FALSE) ## Weather resumes identically wth <- restart_from_log(run.id = "trial2024") ## End(Not run)
Join scan predictions to coordinates
scan_spatial_table( scan, coords, lon_col = "lon", lat_col = "lat", date_col = NULL, genotypes = NULL )scan_spatial_table( scan, coords, lon_col = "lon", lat_col = "lat", date_col = NULL, genotypes = NULL )
scan |
a 'scan_untested_envs' (or [grid_scan()]) result. |
coords |
data.frame with ‘env' matching the scan’s new environments, plus longitude and latitude columns. |
lon_col, lat_col
|
coordinate column names. |
date_col |
optional column giving a planting date per site, for (C). |
genotypes |
optional subset. |
long data.frame: 'gid', 'env', 'lon', 'lat', ['date'], 'yHat', 'lower', 'upper', 'position', 'mahalanobis_ratio', 'prop_outside', 'weight_negativity', 'prob_win' (posterior probability the genotype is the best at that site; 'NA' for scans that do not carry it).
Transfers a fitted kernel_model to environments that were never
planted – new locations, new planting dates, or both – using only their
environmental covariates. No re-fitting and no phenotypes at the new sites
are required.
The environmental main effect estimated at the training sites is kriged onto each new site through a linear environmental kernel, and the whole posterior is carried draw-by-draw so that credible intervals, win probabilities and an extrapolation diagnostic are all internally consistent.
The transfer. Covariates are standardised on the training moments,
and a linear (product) environmental kernel is formed from the
covariates,
The new-site environmental effects are a kriging (Gaussian-process conditional-mean) map of the training effects,
applied to every retained MCMC draw . When a GxE kernel is present its
interaction effects are kriged the same way. The ridge
stabilises the inverse; is not constrained to be positive, and
large negative row weights are the signal that a site is being reached by
extrapolation (see weight_negativity below).
The prediction. For draw , genotype and new site
,
with the training grand mean. Point predictions are posterior means
over draws and the credible interval uses the empirical quantiles at
and . The probability that a
genotype is the best at a site is the fraction of draws in which it attains
the row maximum, so it respects the genotype correlation within a
draw. Per-site genomic heritability is reported as
.
scan_untested_envs( object, W_train, W_new, K_G = NULL, standardize = TRUE, lambda = 1e-06, level = 0.95, max_draws = 500, envelope = c("keep", "flag", "mask", "drop"), envelope_level = c("gross", "mild") )scan_untested_envs( object, W_train, W_new, K_G = NULL, standardize = TRUE, lambda = 1e-06, level = 0.95, max_draws = 500, envelope = c("keep", "flag", "mask", "drop"), envelope_level = c("gross", "mild") )
object |
a fitted model from |
W_train |
training environmental covariate matrix,
environments |
W_new |
new-environment covariate matrix, sites |
K_G |
optional genomic relationship matrix. Used only to inform the GxE transfer; when omitted, GxE is kriged from environmental similarity alone (a warning is issued). |
standardize |
logical. Standardise covariates on the training mean and
SD before building the kernels. Default |
lambda |
numeric ridge added to |
level |
credible-interval mass. Default |
max_draws |
integer. Cap on the number of MCMC draws used; draws are
thinned by even spacing when the chain is longer. Default |
envelope |
what to do about sites that fall outside the training
covariate envelope: |
envelope_level |
which sites count as flagged: |
Extrapolation diagnostics. The scan reports where each new site sits relative to the training covariate cloud. Several complementary measures are returned because no single one is sufficient:
Cosine similarity to the nearest training site,
, summarised as
max_similarity and mean_similarity.
A Mahalanobis-type distance in the leading principal-component
subspace (enough PCs to reach 95% variance),
, divided by the 95th percentile of the
training distances to give mahalanobis_ratio.
A marginal range test per covariate (prop_outside,
max_exceedance_sd). This is marginal: a site inside every
covariate's range individually can still occupy a combination that
never occurred, which only the distance and weight measures catch.
Kriging-weight geometry: weight_negativity
and the effective number of training sites
leaned on, .
A combined divergence_index
with
.
Sites are labelled "interpolation", "edge",
"mild_extrapolation" or "gross_extrapolation" from these.
Position is geometry, not a correctness guarantee. Calibration
against 60 held-out environments found the envelope measures correlate only
to with realised accuracy. Treat the
position label as a description of where a site lies, never as a verdict that
the prediction is right or wrong. The model-internal ci_width_ratio
was the strongest single accuracy correlate (); to know your
true accuracy, run leave-environments-out cross-validation on your own data.
An object of class "scan_untested_envs": a list with
yHat_matrix, yHat_lower, yHat_upper
genotype
new-site matrices of the posterior mean and credible bounds.
yHatthe same in long form, with ci_width.
yHat_prob_winprobability each genotype is best at each site.
genetic_varper-site genetic variance, its interval and
genomic_h2.
vcov, cor
between-new-site genetic covariance and correlation matrices.
interpolation, divergence, diagnostics
the extrapolation measures described above, one row per new site.
exceedance_detailone row per (site offending
covariate) that fell outside the training range.
excludedsites masked or dropped by envelope, else
NULL.
metadimensions, settings, similarity and kriging weights.
Costa-Neto, G., Fritsche-Neto, R. & Crossa, J. (2021) Nonlinear kernels, dominance, and envirotyping data increase the accuracy of genome-based prediction in multi-environment trials. Heredity 126, 92-106.
Jarquin, D. et al. (2014) A reaction norm model for genomic selection using high-dimensional genomic and environmental data. Theoretical and Applied Genetics 127, 595-607.
Cressie, N. (1993) Statistics for Spatial Data. Wiley. (kriging as a Gaussian-process conditional mean.)
kernel_model, get_kernel,
grid_scan, map_scan,
plot_planting_window, best_planting_date
data("maizeWTH"); data("maizeYield"); data("maizeG") ## Training covariates (first 100 days after sowing) vars <- c("FRUE", "PETP", "GDD", "T2M_MAX") Wtr <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = vars, statistic = "mean") ## Fit KEEPING the effect chains -- required by the scan K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = Wtr)[[2]]), data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value") fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", keep_effects = TRUE) ## Predict the trained genotypes at new sites (reuse the training W here) sc <- scan_untested_envs(fit, W_train = Wtr, W_new = Wtr, K_G = maizeG) sc # print method: per-site diagnostics head(sc$yHat) # predictions + credible intervals sc$interpolation[, c("env", "position", "mahalanobis_ratio")]data("maizeWTH"); data("maizeYield"); data("maizeG") ## Training covariates (first 100 days after sowing) vars <- c("FRUE", "PETP", "GDD", "T2M_MAX") Wtr <- W_matrix(env.data = maizeWTH[maizeWTH$daysFromStart < 100, ], var.id = vars, statistic = "mean") ## Fit KEEPING the effect chains -- required by the scan K <- get_kernel(K_G = list(G = maizeG), K_E = list(W = env_kernel(env.data = Wtr)[[2]]), data = maizeYield, model = "RNMM", env = "env", gid = "gid", y = "value") fit <- kernel_model(y = "value", data = maizeYield, random = K, env = "env", gid = "gid", keep_effects = TRUE) ## Predict the trained genotypes at new sites (reuse the training W here) sc <- scan_untested_envs(fit, W_train = Wtr, W_new = Wtr, K_G = maizeG) sc # print method: per-site diagnostics head(sc$yHat) # predictions + credible intervals sc$interpolation[, c("env", "position", "mahalanobis_ratio")]
Generates an environmental covariable matrix (environments in rows,
covariables in columns) whose diversity among environments is set by explicit
knobs: the structure (unstructured, mega-environment clusters, or a continuous
gradient), the redundancy among covariables and their marginal shape. The
induced environment correlation and environmental kernel are attached, so the
result feeds sim_met (as W or C) and
get_kernel directly. It is the environmental analogue of
sim_markers.
sim_envirome( n_env, n_var = 20L, structure = c("random", "clusters", "gradient"), n_cluster = 3L, between.cor = 0.2, spread = 1, redundancy = 0, n_blocks = 1L, marginal = c("gaussian", "right-skewed", "heavy-tailed", "bounded"), env_names = NULL, var_names = NULL, seed = NULL, verbose = TRUE )sim_envirome( n_env, n_var = 20L, structure = c("random", "clusters", "gradient"), n_cluster = 3L, between.cor = 0.2, spread = 1, redundancy = 0, n_blocks = 1L, marginal = c("gaussian", "right-skewed", "heavy-tailed", "bounded"), env_names = NULL, var_names = NULL, seed = NULL, verbose = TRUE )
n_env |
integer. Number of environments (rows, >= 2). |
n_var |
integer. Number of covariables (columns, >= 2). |
structure |
character. |
n_cluster |
integer. Number of mega-environments when
|
between.cor |
numeric in [0, 1). Similarity between clusters (0 = well separated). |
spread |
numeric. Within-cluster / along-gradient dispersion; larger values make environments within a group more diverse. |
redundancy |
numeric in [0, 1). Share of covariables collapsed onto drivers (collinearity), lowering effective rank. |
n_blocks |
integer. Number of covariable drivers when
|
marginal |
character. Marginal of the covariables: |
env_names, var_names
|
character. Optional names for rows / columns. |
seed |
integer. RNG seed. The caller's RNG stream is restored on exit. |
verbose |
logical. Print a diversity summary. Default |
Each environment gets a latent profile that drives its covariables; the
structure controls how those profiles are arranged.
"random" places environments independently (maximal diversity),
"clusters" groups them into n_cluster mega-environments (high
within-cluster similarity, between-cluster similarity set by
between.cor), and "gradient" orders environments along a latent
continuum (neighbours similar, extremes different), as for a latitude or
season gradient. redundancy collapses a share of covariables onto
n_blocks drivers, lowering the effective rank, and marginal
reshapes the covariables away from Gaussian.
A matrix of class "sim_envirome" (environments x covariables).
Attributes: "cluster" (mega-environment of each environment, when
applicable), "C_env" (the induced environment correlation), "K_E"
(the environmental kernel) and "diversity" (mean_similarity off
the environment correlation and eff_dim, the effective dimension of the
environmental kernel).
sim_W, sim_markers, sim_met,
env_kernel
## High diversity: unstructured environments E_hi <- sim_envirome(20, n_var = 30, structure = "random", seed = 1) ## Mega-environments: three tight clusters E_cl <- sim_envirome(20, n_var = 30, structure = "clusters", n_cluster = 3, between.cor = 0.1, seed = 1) attr(E_cl, "diversity") ## A latitudinal / seasonal gradient E_gr <- sim_envirome(20, n_var = 30, structure = "gradient", seed = 1) ## Feed into sim_met as the envirome (reaction norm) or as a target C met <- sim_met(markers = sim_markers(100, 500, seed = 1), W = as.matrix(E_cl), C = attr(E_cl, "C_env"), seed = 1) KE <- list(W = attr(E_cl, "K_E")) ## Contrast G x E under low vs high environmental diversity (genetics fixed) X <- sim_markers(150, 2000, seed = 1) g_hi <- cor(sim_met(markers = X, C = attr(E_hi, "C_env"), seed = 1)$truth$g) g_cl <- cor(sim_met(markers = X, C = attr(E_cl, "C_env"), seed = 1)$truth$g) c(diverse = mean(g_hi[lower.tri(g_hi)]), # low genetic cor -> more G x E clustered = mean(g_cl[lower.tri(g_cl)]))## High diversity: unstructured environments E_hi <- sim_envirome(20, n_var = 30, structure = "random", seed = 1) ## Mega-environments: three tight clusters E_cl <- sim_envirome(20, n_var = 30, structure = "clusters", n_cluster = 3, between.cor = 0.1, seed = 1) attr(E_cl, "diversity") ## A latitudinal / seasonal gradient E_gr <- sim_envirome(20, n_var = 30, structure = "gradient", seed = 1) ## Feed into sim_met as the envirome (reaction norm) or as a target C met <- sim_met(markers = sim_markers(100, 500, seed = 1), W = as.matrix(E_cl), C = attr(E_cl, "C_env"), seed = 1) KE <- list(W = attr(E_cl, "K_E")) ## Contrast G x E under low vs high environmental diversity (genetics fixed) X <- sim_markers(150, 2000, seed = 1) g_hi <- cor(sim_met(markers = X, C = attr(E_hi, "C_env"), seed = 1)$truth$g) g_cl <- cor(sim_met(markers = X, C = attr(E_cl, "C_env"), seed = 1)$truth$g) c(diverse = mean(g_hi[lower.tri(g_hi)]), # low genetic cor -> more G x E clustered = mean(g_cl[lower.tri(g_cl)]))
Generates a bi-allelic SNP dosage matrix (lines in rows, markers in columns)
whose genetic diversity is set by explicit knobs: the minor-allele-frequency
spectrum, population structure ( across subpopulations) and
linkage disequilibrium (redundant marker blocks). The VanRaden genomic
relationship matrix is attached, so the result feeds sim_met
(as markers or via the kinship) and get_kernel directly.
sim_markers( n_lines, n_marker = 1000L, maf = c("uniform", "rare", "common"), n_subpop = 1L, Fst = 0, ld = 0, ld_blocks = 1L, line_names = NULL, seed = NULL, verbose = TRUE )sim_markers( n_lines, n_marker = 1000L, maf = c("uniform", "rare", "common"), n_subpop = 1L, Fst = 0, ld = 0, ld_blocks = 1L, line_names = NULL, seed = NULL, verbose = TRUE )
n_lines |
integer. Number of lines (rows, >= 2). |
n_marker |
integer. Number of markers (columns). |
maf |
character. Minor-allele-frequency spectrum: |
n_subpop |
integer. Number of subpopulations. Lines are assigned in round-robin order. |
Fst |
numeric in [0, 1). Between-subpopulation differentiation. |
ld |
numeric in [0, 1). Fraction of each block's markers forced to equal the block lead marker, creating linkage disequilibrium / redundancy. |
ld_blocks |
integer. Number of linkage blocks when |
line_names |
character. Optional line names (default |
seed |
integer. RNG seed. The caller's RNG stream is restored on exit. |
verbose |
logical. Print a diversity summary. Default |
Allele frequencies are drawn per marker and genotypes as
. Population structure follows the
Balding–Nichols model: within subpopulation the frequency drifts
from the ancestral as
, so larger means more
between-group differentiation and lower within-group diversity. Linkage
disequilibrium is emulated by copying each block's lead marker into a fraction
ld of the other markers in the block, lowering the effective rank of
the kinship.
An integer matrix of class "sim_markers" (lines x markers). Attributes:
"freq" (per-marker allele frequency), "subpop" (subpopulation of
each line), "G" (the VanRaden kinship, positive definite) and
"diversity" (He expected heterozygosity, mean_relatedness
and eff_dim, the effective dimension of the kinship).
sim_met, sim_envirome, get_kernel
## High diversity: many common markers, one panmictic population X_hi <- sim_markers(150, 2000, maf = "common", seed = 1) ## Low diversity: fewer, rarer markers X_lo <- sim_markers(150, 300, maf = "rare", seed = 1) ## Structured: four subpopulations with strong drift X_st <- sim_markers(150, 2000, n_subpop = 4, Fst = 0.15, seed = 1) attr(X_st, "diversity") ## Feed straight into sim_met (markers mechanism) or via the kinship C <- sim_met_C(q = 10, structure = "ar1", rho = 0.6) met <- sim_met(markers = X_st, C = C, n.qtl = 50, seed = 1) KG <- list(G = attr(X_st, "G")) ## Compare phenotype variance under low vs high genetic diversity E <- sim_envirome(10, 30, structure = "clusters", seed = 1) v_lo <- var(sim_met(markers = X_lo, C = attr(E, "C_env"), seed = 1)$data$value) v_hi <- var(sim_met(markers = X_hi, C = attr(E, "C_env"), seed = 1)$data$value) c(low = v_lo, high = v_hi)## High diversity: many common markers, one panmictic population X_hi <- sim_markers(150, 2000, maf = "common", seed = 1) ## Low diversity: fewer, rarer markers X_lo <- sim_markers(150, 300, maf = "rare", seed = 1) ## Structured: four subpopulations with strong drift X_st <- sim_markers(150, 2000, n_subpop = 4, Fst = 0.15, seed = 1) attr(X_st, "diversity") ## Feed straight into sim_met (markers mechanism) or via the kinship C <- sim_met_C(q = 10, structure = "ar1", rho = 0.6) met <- sim_met(markers = X_st, C = C, n.qtl = 50, seed = 1) KG <- list(G = attr(X_st, "G")) ## Compare phenotype variance under low vs high genetic diversity E <- sim_envirome(10, 30, structure = "clusters", seed = 1) v_lo <- var(sim_met(markers = X_lo, C = attr(E, "C_env"), seed = 1)$data$value) v_hi <- var(sim_met(markers = X_hi, C = attr(E, "C_env"), seed = 1)$data$value) c(low = v_lo, high = v_hi)
Core simulator. Generates genotype-by-environment phenotypes whose truth is
known exactly: a genomic kinship among lines, a genetic (type-B)
correlation among environments, and a per-environment heritability.
It is the reference-answer generator against which kernel_model
and scan_untested_envs can be checked, and it pairs with
sim_W to study how well an envirome recovers .
sim_met( K = NULL, n_env = NULL, min.h2 = 0.2, max.h2 = 0.8, min.cor = 0, max.cor = 0.8, C = NULL, h2 = NULL, n_rep = 1, sg2 = 1, exact = TRUE, env_effects = NULL, subset = NULL, subset.by = c("cell", "gid_env"), K.weights = NULL, W = NULL, reaction.norm = !is.null(W), rn.main = 0.5, gxe.specific = 0, markers = NULL, n.qtl = NULL, rep.var = 0, spatial = FALSE, field.dim = NULL, spatial.rho = 0.6, spatial.var = 1, seed = NULL, verbose = TRUE )sim_met( K = NULL, n_env = NULL, min.h2 = 0.2, max.h2 = 0.8, min.cor = 0, max.cor = 0.8, C = NULL, h2 = NULL, n_rep = 1, sg2 = 1, exact = TRUE, env_effects = NULL, subset = NULL, subset.by = c("cell", "gid_env"), K.weights = NULL, W = NULL, reaction.norm = !is.null(W), rn.main = 0.5, gxe.specific = 0, markers = NULL, n.qtl = NULL, rep.var = 0, spatial = FALSE, field.dim = NULL, spatial.rho = 0.6, spatial.var = 1, seed = NULL, verbose = TRUE )
K |
n x n genomic kinship AMONG LINES (e.g. |
n_env |
integer. Number of environments; inferred from |
min.h2, max.h2
|
numeric. Plot-basis heritability range, used when
|
min.cor, max.cor
|
numeric. Genetic correlation range among environments,
used when |
C |
q x q correlation AMONG ENVIRONMENTS (see |
h2 |
numeric vector of per-environment heritabilities (length 1 or
|
n_rep |
integer. Replicates per genotype x environment cell. |
sg2 |
numeric. Genetic variance per environment (length 1 or |
exact |
logical. Orthonormalise the draw so realised moments match the
targets. Default |
env_effects |
numeric. Optional fixed environment means |
subset |
numeric in (0, 1]. Fraction of cells retained, to induce unbalancedness. |
subset.by |
"cell" or "gid_env". Whether missingness is drawn per observation or per genotype-by-environment cell. |
K.weights |
numeric. Non-negative weights (rescaled to sum 1) for a list
of kinships in |
W |
q x k envirome matrix. When supplied, genetic values are generated as
reaction norms and the environment correlation is induced from |
reaction.norm |
logical. Use the reaction-norm mechanism. Defaults to
|
rn.main |
numeric in [0, 1]. Share of genetic variance assigned to the main (across-environment) effect under the reaction-norm mechanism; the rest is reaction-norm interaction. Default 0.5. |
gxe.specific |
numeric in [0, 1). Share of genetic variance made line-independent and environment-specific, breaking separability. Default 0. |
markers |
n x m marker matrix. When supplied, a marker/QTL mechanism
replaces the infinitesimal draw and |
n.qtl |
integer. Number of markers acting as QTL (sampled) when
|
rep.var |
numeric. Variance of a random replicate/block effect added to phenotypes. Default 0. |
spatial |
logical. Add an AR1 |
field.dim |
integer length-2 |
spatial.rho |
numeric. Lag-1 spatial autocorrelation. Default 0.6. |
spatial.var |
numeric. Variance of the spatial field. Default 1. |
seed |
integer. RNG seed. The caller's RNG stream is restored on exit. |
verbose |
logical. Print a summary. Default |
Model. For lines and environments, genetic values are
drawn from a separable (Kronecker) Gaussian
realised as with ,
. When exact = TRUE the driver is
orthonormalised so that , making the realised moments
match their targets instead of merely scattering around them.
Phenotypes. For replicate ,
Heritability, two bases. The plot-basis value
is what min.h2/max.h2 target; the
line-mean basis is also reported.
Realised environment correlation is measured on the K-whitened scale.
Because lines are related through , cor(G) across lines is not
; forcing it to be is wrong. The realised correlation is defined as
and stored for validation.
The two matrices are different objects. K is
among lines; the environment correlation is . A genomic kinship
passed where an environment correlation is expected is the most common error
and is rejected by an elementwise unit-diagonal check.
Alternative genetic mechanisms. Besides the default separable draw, three opt-in mechanisms generate the genetic values:
Reaction norm (supply W): genetic values are built
causally from an envirome, with a
main effect and line sensitivities both
-structured. The environment correlation is then induced by
W (and reported), not imposed. rn.main splits variance between
the main effect and the reaction-norm interaction.
Marker-based (supply markers): QTL effects are sampled per
environment with the target correlation C, giving large-effect
architectures instead of the infinitesimal draw.
Multiple kinships (supply a list for K with
K.weights): independent additive, dominance or epistatic components
are summed.
gxe.specific adds a line-independent, environment-specific deviation that
makes the covariance non-separable. rep.var and spatial
(an AR1AR1 field) add trial realism to the phenotypes.
An object of class "sim_met": a list with
datalong data.frame with env, gid,
rep, value.
C_envthe target environment correlation, with the realised
correlation attached as attribute "realised".
truthlist of everything known: genetic values g,
target/realised correlations, target/realised heritabilities on both
bases, variances, means.
Kthe positive-definite kinship actually used.
Costa-Neto, G., et al. (2023). Envirome-wide associations enhance multi-environment prediction. G3 13(2), jkac313.
sim_W, sim_met_C, env_cor,
sim_markers, sim_envirome,
kernel_model, scan_untested_envs
data(maizeG) ## 1. Baseline: random C and random h2 within a range met <- sim_met(maizeG, n_env = 10, min.cor = 0.2, max.cor = 0.8, min.h2 = 0.3, max.h2 = 0.7, seed = 1) head(met$data) env_cor(met, "realised") ## 2. Supply an explicit environment correlation (e.g. a year-like series) C <- sim_met_C(q = 8, structure = "ar1", rho = 0.6) met <- sim_met(maizeG, C = C, h2 = 0.5, seed = 1) ## 3. Fixed per-environment heritabilities and environment means met <- sim_met(maizeG, n_env = 4, h2 = c(0.3, 0.5, 0.6, 0.8), env_effects = c(2, 4, 6, 8), seed = 1) ## 4. Replicates and an unbalanced design (retain 70% of g x e cells) met <- sim_met(maizeG, n_env = 6, n_rep = 3, subset = 0.7, subset.by = "gid_env", seed = 1) ## 5. Several genetic variance components (additive + dominance + epistasis) met <- sim_met(K = list(add = maizeG, dom = maizeG, epi = maizeG), K.weights = c(0.6, 0.3, 0.1), n_env = 8, seed = 1) ## 6. Causal reaction-norm: C is INDUCED by the envirome, not imposed Wenv <- scale(matrix(rnorm(10 * 6), 10, 6)) rn <- sim_met(maizeG, W = Wenv, rn.main = 0.4, seed = 1) env_cor(rn, "target") ## 7. Marker / QTL architecture instead of the infinitesimal draw X <- matrix(rbinom(200 * 500, 2, 0.3), 200, 500) qtl <- sim_met(markers = X, n_env = 6, n.qtl = 30, seed = 1) ## 8. Non-separable GxE: 25% of genetic variance is environment-specific met <- sim_met(maizeG, n_env = 6, gxe.specific = 0.25, seed = 1) ## 9. Field realism: replicate/block noise plus an AR1 x AR1 spatial trend met <- sim_met(maizeG, n_env = 4, n_rep = 2, rep.var = 0.5, spatial = TRUE, field.dim = c(20, 15), spatial.rho = 0.7, spatial.var = 1.5, seed = 1) ## 10. Pair with a simulated envirome and diagnose the recovery W <- sim_W(met$C_env, noise = 0.3, n_var = 50, seed = 1) plot(met) ## 11. End-to-end: simulate kinship + MET + envirome, then fit a model ## Simulate a genomic kinship KG from markers (VanRaden) X <- matrix(rbinom(150 * 1000, 2, 0.3), 150, 1000) rownames(X) <- paste0("G", seq_len(150)) # gid names carry through Z <- scale(X); Z[is.na(Z)] <- 0 Kg <- tcrossprod(Z) / ncol(Z) + diag(1e-6, 150) # positive-definite kinship dimnames(Kg) <- list(rownames(X), rownames(X)) KG <- list(G = Kg) ## Simulate the MET truth on that kinship (one replicate) met <- sim_met(Kg, n_env = 10, n_rep = 1, min.cor = 0.3, max.cor = 0.8, min.h2 = 0.3, max.h2 = 0.7, seed = 1) df <- as.data.frame(met$data) # base data.frame for get_kernel() ## Simulate a redundant, noisier envirome on the same environments W2 <- sim_W(met$C_env, noise = 0.6, n_var = 200, collinearity = 0.8, n_blocks = 5, seed = 1) KE2 <- list(W = env_kernel(env.data = as.matrix(W2))[[2]]) ## Build reaction-norm kernels and fit the Bayesian model K2 <- get_kernel(K_G = KG, K_E = KE2, data = df, model = "RNMM", env = "env", gid = "gid", y = "value") fit <- kernel_model(y = "value", data = df, random = K2, env = "env", gid = "gid", iterations = 5000, burnin = 1000) ## 12. Phenotypes for contrasting GENETIC diversity (envirome fixed) ## Low diversity: few rare markers, strong structure; high: many common. E <- sim_envirome(12, 40, structure = "clusters", n_cluster = 3, seed = 1) X_lo <- sim_markers(200, 500, maf = "rare", n_subpop = 5, Fst = 0.2, seed = 1) X_hi <- sim_markers(200, 4000, maf = "common", n_subpop = 1, Fst = 0.0, seed = 1) met_lo <- sim_met(markers = X_lo, C = attr(E, "C_env"), n.qtl = 80, seed = 1) met_hi <- sim_met(markers = X_hi, C = attr(E, "C_env"), n.qtl = 80, seed = 1) c(low = var(met_lo$data$value), high = var(met_hi$data$value)) ## 13. Phenotypes for contrasting ENVIROME diversity (genetics fixed) ## Diverse environments induce lower cross-environment correlation (more GxE). X <- sim_markers(200, 3000, n_subpop = 3, Fst = 0.08, seed = 2) E_lo <- sim_envirome(12, 40, structure = "gradient", redundancy = 0.7, n_blocks = 2, seed = 2) E_hi <- sim_envirome(12, 40, structure = "random", redundancy = 0, seed = 2) met_eLo <- sim_met(markers = X, C = attr(E_lo, "C_env"), n.qtl = 80, seed = 2) met_eHi <- sim_met(markers = X, C = attr(E_hi, "C_env"), n.qtl = 80, seed = 2) cor(met_eLo$truth$g)[1, 2] # high cross-environment genetic correlation cor(met_eHi$truth$g)[1, 2] # low correlation -> stronger GxE ## 14. Full 2 x 2 factorial of genetic x envirome diversity scenario <- function(gen, env, seed = 1) { X <- if (gen == "low") sim_markers(200, 500, maf = "rare", n_subpop = 5, Fst = 0.2, seed = seed, verbose = FALSE) else sim_markers(200, 4000, maf = "common", n_subpop = 1, Fst = 0, seed = seed, verbose = FALSE) E <- if (env == "low") sim_envirome(12, 40, structure = "gradient", redundancy = 0.7, n_blocks = 2, seed = seed, verbose = FALSE) else sim_envirome(12, 40, structure = "random", redundancy = 0, seed = seed, verbose = FALSE) met <- sim_met(markers = X, C = attr(E, "C_env"), n.qtl = 80, min.h2 = 0.3, max.h2 = 0.6, seed = seed) Cg <- cor(met$truth$g) data.frame(gen = gen, env = env, gxe_cor = mean(Cg[lower.tri(Cg)]), pheno_var = var(met$data$value)) } do.call(rbind, list(scenario("low", "low"), scenario("low", "high"), scenario("high", "low"), scenario("high", "high"))) ## 15. Sweep population structure (Fst) and record phenotype variance E <- sim_envirome(10, 40, structure = "clusters", n_cluster = 3, seed = 3) do.call(rbind, lapply(c(0, 0.05, 0.1, 0.2), function(f) { X <- sim_markers(200, 3000, n_subpop = 4, Fst = f, seed = 3, verbose = FALSE) met <- sim_met(markers = X, C = attr(E, "C_env"), n.qtl = 80, seed = 3) data.frame(Fst = f, eff_dim = attr(X, "diversity")["eff_dim"], pheno_var = var(met$data$value)) })) ## 16. Reaction-norm phenotypes driven directly by a diverse envirome X <- sim_markers(150, 2500, n_subpop = 2, Fst = 0.06, seed = 6) E2 <- sim_envirome(10, 25, structure = "gradient", marginal = "right-skewed", seed = 6) met2 <- sim_met(markers = X, W = as.matrix(E2), reaction.norm = TRUE, rn.main = 0.6, seed = 6) met2$truth$mechanismdata(maizeG) ## 1. Baseline: random C and random h2 within a range met <- sim_met(maizeG, n_env = 10, min.cor = 0.2, max.cor = 0.8, min.h2 = 0.3, max.h2 = 0.7, seed = 1) head(met$data) env_cor(met, "realised") ## 2. Supply an explicit environment correlation (e.g. a year-like series) C <- sim_met_C(q = 8, structure = "ar1", rho = 0.6) met <- sim_met(maizeG, C = C, h2 = 0.5, seed = 1) ## 3. Fixed per-environment heritabilities and environment means met <- sim_met(maizeG, n_env = 4, h2 = c(0.3, 0.5, 0.6, 0.8), env_effects = c(2, 4, 6, 8), seed = 1) ## 4. Replicates and an unbalanced design (retain 70% of g x e cells) met <- sim_met(maizeG, n_env = 6, n_rep = 3, subset = 0.7, subset.by = "gid_env", seed = 1) ## 5. Several genetic variance components (additive + dominance + epistasis) met <- sim_met(K = list(add = maizeG, dom = maizeG, epi = maizeG), K.weights = c(0.6, 0.3, 0.1), n_env = 8, seed = 1) ## 6. Causal reaction-norm: C is INDUCED by the envirome, not imposed Wenv <- scale(matrix(rnorm(10 * 6), 10, 6)) rn <- sim_met(maizeG, W = Wenv, rn.main = 0.4, seed = 1) env_cor(rn, "target") ## 7. Marker / QTL architecture instead of the infinitesimal draw X <- matrix(rbinom(200 * 500, 2, 0.3), 200, 500) qtl <- sim_met(markers = X, n_env = 6, n.qtl = 30, seed = 1) ## 8. Non-separable GxE: 25% of genetic variance is environment-specific met <- sim_met(maizeG, n_env = 6, gxe.specific = 0.25, seed = 1) ## 9. Field realism: replicate/block noise plus an AR1 x AR1 spatial trend met <- sim_met(maizeG, n_env = 4, n_rep = 2, rep.var = 0.5, spatial = TRUE, field.dim = c(20, 15), spatial.rho = 0.7, spatial.var = 1.5, seed = 1) ## 10. Pair with a simulated envirome and diagnose the recovery W <- sim_W(met$C_env, noise = 0.3, n_var = 50, seed = 1) plot(met) ## 11. End-to-end: simulate kinship + MET + envirome, then fit a model ## Simulate a genomic kinship KG from markers (VanRaden) X <- matrix(rbinom(150 * 1000, 2, 0.3), 150, 1000) rownames(X) <- paste0("G", seq_len(150)) # gid names carry through Z <- scale(X); Z[is.na(Z)] <- 0 Kg <- tcrossprod(Z) / ncol(Z) + diag(1e-6, 150) # positive-definite kinship dimnames(Kg) <- list(rownames(X), rownames(X)) KG <- list(G = Kg) ## Simulate the MET truth on that kinship (one replicate) met <- sim_met(Kg, n_env = 10, n_rep = 1, min.cor = 0.3, max.cor = 0.8, min.h2 = 0.3, max.h2 = 0.7, seed = 1) df <- as.data.frame(met$data) # base data.frame for get_kernel() ## Simulate a redundant, noisier envirome on the same environments W2 <- sim_W(met$C_env, noise = 0.6, n_var = 200, collinearity = 0.8, n_blocks = 5, seed = 1) KE2 <- list(W = env_kernel(env.data = as.matrix(W2))[[2]]) ## Build reaction-norm kernels and fit the Bayesian model K2 <- get_kernel(K_G = KG, K_E = KE2, data = df, model = "RNMM", env = "env", gid = "gid", y = "value") fit <- kernel_model(y = "value", data = df, random = K2, env = "env", gid = "gid", iterations = 5000, burnin = 1000) ## 12. Phenotypes for contrasting GENETIC diversity (envirome fixed) ## Low diversity: few rare markers, strong structure; high: many common. E <- sim_envirome(12, 40, structure = "clusters", n_cluster = 3, seed = 1) X_lo <- sim_markers(200, 500, maf = "rare", n_subpop = 5, Fst = 0.2, seed = 1) X_hi <- sim_markers(200, 4000, maf = "common", n_subpop = 1, Fst = 0.0, seed = 1) met_lo <- sim_met(markers = X_lo, C = attr(E, "C_env"), n.qtl = 80, seed = 1) met_hi <- sim_met(markers = X_hi, C = attr(E, "C_env"), n.qtl = 80, seed = 1) c(low = var(met_lo$data$value), high = var(met_hi$data$value)) ## 13. Phenotypes for contrasting ENVIROME diversity (genetics fixed) ## Diverse environments induce lower cross-environment correlation (more GxE). X <- sim_markers(200, 3000, n_subpop = 3, Fst = 0.08, seed = 2) E_lo <- sim_envirome(12, 40, structure = "gradient", redundancy = 0.7, n_blocks = 2, seed = 2) E_hi <- sim_envirome(12, 40, structure = "random", redundancy = 0, seed = 2) met_eLo <- sim_met(markers = X, C = attr(E_lo, "C_env"), n.qtl = 80, seed = 2) met_eHi <- sim_met(markers = X, C = attr(E_hi, "C_env"), n.qtl = 80, seed = 2) cor(met_eLo$truth$g)[1, 2] # high cross-environment genetic correlation cor(met_eHi$truth$g)[1, 2] # low correlation -> stronger GxE ## 14. Full 2 x 2 factorial of genetic x envirome diversity scenario <- function(gen, env, seed = 1) { X <- if (gen == "low") sim_markers(200, 500, maf = "rare", n_subpop = 5, Fst = 0.2, seed = seed, verbose = FALSE) else sim_markers(200, 4000, maf = "common", n_subpop = 1, Fst = 0, seed = seed, verbose = FALSE) E <- if (env == "low") sim_envirome(12, 40, structure = "gradient", redundancy = 0.7, n_blocks = 2, seed = seed, verbose = FALSE) else sim_envirome(12, 40, structure = "random", redundancy = 0, seed = seed, verbose = FALSE) met <- sim_met(markers = X, C = attr(E, "C_env"), n.qtl = 80, min.h2 = 0.3, max.h2 = 0.6, seed = seed) Cg <- cor(met$truth$g) data.frame(gen = gen, env = env, gxe_cor = mean(Cg[lower.tri(Cg)]), pheno_var = var(met$data$value)) } do.call(rbind, list(scenario("low", "low"), scenario("low", "high"), scenario("high", "low"), scenario("high", "high"))) ## 15. Sweep population structure (Fst) and record phenotype variance E <- sim_envirome(10, 40, structure = "clusters", n_cluster = 3, seed = 3) do.call(rbind, lapply(c(0, 0.05, 0.1, 0.2), function(f) { X <- sim_markers(200, 3000, n_subpop = 4, Fst = f, seed = 3, verbose = FALSE) met <- sim_met(markers = X, C = attr(E, "C_env"), n.qtl = 80, seed = 3) data.frame(Fst = f, eff_dim = attr(X, "diversity")["eff_dim"], pheno_var = var(met$data$value)) })) ## 16. Reaction-norm phenotypes driven directly by a diverse envirome X <- sim_markers(150, 2500, n_subpop = 2, Fst = 0.06, seed = 6) E2 <- sim_envirome(10, 25, structure = "gradient", marginal = "right-skewed", seed = 6) met2 <- sim_met(markers = X, W = as.matrix(E2), reaction.norm = TRUE, rn.main = 0.6, seed = 6) met2$truth$mechanism
Accessory to sim_met and sim_W. Builds a valid
correlation matrix among environments, so the same C
can seed both simulators explicitly.
sim_met_C( q, min.cor = 0, max.cor = 0.8, structure = c("random", "vine", "equi", "ar1", "block", "factor"), rho = NULL, n_blocks = 2L, between.cor = 0, n_factor = 2L, env_names = NULL, max.tries = 200L, seed = NULL )sim_met_C( q, min.cor = 0, max.cor = 0.8, structure = c("random", "vine", "equi", "ar1", "block", "factor"), rho = NULL, n_blocks = 2L, between.cor = 0, n_factor = 2L, env_names = NULL, max.tries = 200L, seed = NULL )
q |
integer. Number of environments (>= 3). |
min.cor, max.cor
|
numeric. Range for off-diagonal correlations. For the structured generators these bound the random inputs (vine partial correlations, factor loadings) rather than the realised off-diagonals exactly. |
structure |
character. How the matrix is built: |
rho |
numeric or |
n_blocks |
integer. Number of mega-environments when |
between.cor |
numeric. Between-block correlation when |
n_factor |
integer. Number of latent factors when |
env_names |
character. Optional environment names. |
max.tries |
integer. Rejection-sampling attempts before repair. |
seed |
integer. RNG seed. |
Sampling off-diagonals uniformly from [min.cor, max.cor] does not
generally give a positive semi-definite matrix. The equicorrelated bound is
, so for nothing below is
attainable. A naive eigenvalue repair silently returns something else, so this
function errors up front when the request is infeasible and rejection-samples
otherwise, only falling back to eigenvalue clamping (with a warning) if
sampling fails.
A correlation matrix of class "C_env" with a
logical attribute "repaired" and an attribute "requested"
recording the target range.
C <- sim_met_C(q = 8, min.cor = 0.2, max.cor = 0.8, seed = 1) range(C[lower.tri(C)]) ## Structured mega-environments and an autoregressive year series Cb <- sim_met_C(q = 9, structure = "block", n_blocks = 3, rho = 0.7, between.cor = 0.1) Car <- sim_met_C(q = 8, structure = "ar1", rho = 0.6)C <- sim_met_C(q = 8, min.cor = 0.2, max.cor = 0.8, seed = 1) range(C[lower.tri(C)]) ## Structured mega-environments and an autoregressive year series Cb <- sim_met_C(q = 9, structure = "block", n_blocks = 3, rho = 0.7, between.cor = 0.1) Car <- sim_met_C(q = 8, structure = "ar1", rho = 0.6)
Core simulator. Given a target correlation among environments,
it builds a covariable matrix whose
environmental kernel recovers a known share of . It answers:
if the true similarity among my environments is , what would an
envirome of this size and quality look like, and how much of that similarity
would it recover? Pairs with sim_met.
sim_W( C_env, noise = 0, n_var = 50, collinearity = 0, n_blocks = 1, calibrate = TRUE, cal.nrep = 15L, marginal = c("gaussian", "right-skewed", "heavy-tailed", "bounded"), kernel = c("linear", "rbf"), bandwidth = NULL, na.frac = 0, family.sizes = NULL, var_names = NULL, env_names = NULL, seed = NULL, verbose = TRUE )sim_W( C_env, noise = 0, n_var = 50, collinearity = 0, n_blocks = 1, calibrate = TRUE, cal.nrep = 15L, marginal = c("gaussian", "right-skewed", "heavy-tailed", "bounded"), kernel = c("linear", "rbf"), bandwidth = NULL, na.frac = 0, family.sizes = NULL, var_names = NULL, env_names = NULL, seed = NULL, verbose = TRUE )
C_env |
q x q correlation AMONG ENVIRONMENTS, from any source
(e.g. |
noise |
numeric in [0, 1]. With |
n_var |
integer. Number of covariables (columns of |
collinearity |
numeric in [0, 1). Share of columns collapsed onto drivers
(also the within-family strength when |
n_blocks |
integer. Collinear columns grouped into this many families. |
calibrate |
logical. Solve for the internal variance share |
cal.nrep |
integer. Monte-Carlo replicates used during calibration. |
marginal |
character. Marginal distribution of the covariables:
|
kernel |
character. Environmental kernel: |
bandwidth |
numeric or |
na.frac |
numeric in [0, 1). Fraction of entries set to |
family.sizes |
integer vector (optionally named). Column counts per
covariable family; must sum to |
var_names, env_names
|
character. Optional names for columns / rows. |
seed |
integer. RNG seed. The caller's RNG stream is restored on exit. |
verbose |
logical. Print a summary. Default |
Construction. With the symmetric PSD square root
of the target, an orthonormal driver () and iid
noise ,
and the environmental kernel follows the env_kernel convention
Recovery is measured by the off-diagonal Pearson correlation
.
Calibration. When calibrate = TRUE, noise is on the
outcome scale: the internal variance share is solved by
uniroot so that . Because
decreases in , noise = 0 gives (full
recovery) and noise = 1 gives (pure noise).
Collinearity. With collinearity > 0, a share of columns is
collapsed onto n_blocks driver families, mimicking the redundancy of
real enviromes and lowering the effective rank.
C_env is an environment correlation, not a kinship. It must be
with unit diagonal. Passing a genomic kinship is rejected.
Realism knobs. marginal maps each covariable to a non-Gaussian
shape (rank-preserving), kernel = "rbf" builds a Gaussian instead of a
linear kernel, family.sizes groups columns into named covariable
families (as in real enviromes), and na.frac injects missing values
into the returned after the truth is computed, so downstream
cleaning (e.g. W_matrix imputation) can be tested against a known
answer.
A matrix of class "sim_W". Attributes:
"C_env" (the target), "K_W" (the realised kernel) and
"explained" (a list with r, r2, per-PC variance shares,
effective rank eff_rank, the internal share a_internal, the
marginal/kernel/na.frac used, and the requested/target
values).
Costa-Neto, G., et al. (2023). Envirome-wide associations enhance multi-environment prediction. G3 13(2), jkac313.
sim_met, sim_met_C, sim_W_grid,
env_cor, env_kernel
C <- sim_met_C(q = 10, min.cor = 0.2, max.cor = 0.8, seed = 1) ## 1. An envirome that recovers ~70% of C (noise is on the outcome scale) W <- sim_W(C, noise = 0.3, n_var = 50, seed = 1) attr(W, "explained")$r ## 2. Perfect vs pure-noise enviromes W_good <- sim_W(C, noise = 0.0, n_var = 50) W_noise <- sim_W(C, noise = 1.0, n_var = 50) ## 3. Redundant envirome: many collinear columns, low effective rank W <- sim_W(C, noise = 0.3, n_var = 200, collinearity = 0.8, n_blocks = 5) ## 4. Non-Gaussian covariables (rank-preserving marginal transform) W <- sim_W(C, noise = 0.3, n_var = 60, marginal = "right-skewed") W <- sim_W(C, noise = 0.3, n_var = 60, marginal = "heavy-tailed") W <- sim_W(C, noise = 0.3, n_var = 60, marginal = "bounded") ## 5. Gaussian RBF kernel instead of the linear WW' W <- sim_W(C, noise = 0.3, n_var = 60, kernel = "rbf") W <- sim_W(C, noise = 0.3, n_var = 60, kernel = "rbf", bandwidth = 2) ## 6. Inject missing values AFTER the truth is computed W <- sim_W(C, noise = 0.3, n_var = 60, na.frac = 0.1) sum(is.na(W)) ## 7. Named covariable families (sizes must sum to n_var) W <- sim_W(C, n_var = 12, family.sizes = c(temp = 4, rain = 5, rad = 3)) colnames(W) ## 8. Custom names and a faster/looser calibration W <- sim_W(C, noise = 0.4, n_var = 30, cal.nrep = 5, env_names = paste0("Loc", 1:10), var_names = paste0("bio", 1:30), seed = 1) ## 9. Raw (uncalibrated) internal variance share W <- sim_W(C, noise = 0.5, n_var = 50, calibrate = FALSE) plot(W)C <- sim_met_C(q = 10, min.cor = 0.2, max.cor = 0.8, seed = 1) ## 1. An envirome that recovers ~70% of C (noise is on the outcome scale) W <- sim_W(C, noise = 0.3, n_var = 50, seed = 1) attr(W, "explained")$r ## 2. Perfect vs pure-noise enviromes W_good <- sim_W(C, noise = 0.0, n_var = 50) W_noise <- sim_W(C, noise = 1.0, n_var = 50) ## 3. Redundant envirome: many collinear columns, low effective rank W <- sim_W(C, noise = 0.3, n_var = 200, collinearity = 0.8, n_blocks = 5) ## 4. Non-Gaussian covariables (rank-preserving marginal transform) W <- sim_W(C, noise = 0.3, n_var = 60, marginal = "right-skewed") W <- sim_W(C, noise = 0.3, n_var = 60, marginal = "heavy-tailed") W <- sim_W(C, noise = 0.3, n_var = 60, marginal = "bounded") ## 5. Gaussian RBF kernel instead of the linear WW' W <- sim_W(C, noise = 0.3, n_var = 60, kernel = "rbf") W <- sim_W(C, noise = 0.3, n_var = 60, kernel = "rbf", bandwidth = 2) ## 6. Inject missing values AFTER the truth is computed W <- sim_W(C, noise = 0.3, n_var = 60, na.frac = 0.1) sum(is.na(W)) ## 7. Named covariable families (sizes must sum to n_var) W <- sim_W(C, n_var = 12, family.sizes = c(temp = 4, rain = 5, rad = 3)) colnames(W) ## 8. Custom names and a faster/looser calibration W <- sim_W(C, noise = 0.4, n_var = 30, cal.nrep = 5, env_names = paste0("Loc", 1:10), var_names = paste0("bio", 1:30), seed = 1) ## 9. Raw (uncalibrated) internal variance share W <- sim_W(C, noise = 0.5, n_var = 50, calibrate = FALSE) plot(W)
Accessory to sim_W. Repeatedly simulates enviromes across a grid
of noise, n_var and collinearity, returning the mean
recovery and effective rank at each design point.
sim_W_grid( C_env, noise = seq(0, 1, 0.25), n_var = c(10, 50, 200), collinearity = c(0, 0.5, 0.9), n_rep = 10, calibrate = TRUE, cal.nrep = 15L, seed = NULL, verbose = TRUE )sim_W_grid( C_env, noise = seq(0, 1, 0.25), n_var = c(10, 50, 200), collinearity = c(0, 0.5, 0.9), n_rep = 10, calibrate = TRUE, cal.nrep = 15L, seed = NULL, verbose = TRUE )
C_env |
q x q correlation AMONG ENVIRONMENTS (see |
noise, n_var, collinearity
|
numeric vectors defining the grid. |
n_rep |
integer. Replicate simulations per design point. |
calibrate |
logical. Passed to |
cal.nrep |
integer. Calibration replicates per |
seed |
integer. RNG seed; each design point gets a reproducible sub-seed. |
verbose |
logical. Print per-design-point progress. Default |
A data.frame with one row per design point: noise,
n_var, collinearity, mean recovery r and its SD
r_sd, r2, and mean eff_rank.
C <- sim_met_C(q = 10, min.cor = 0.2, max.cor = 0.8, seed = 1) grid <- sim_W_grid(C, noise = c(0, 0.3, 0.6), n_var = c(20, 100), collinearity = c(0, 0.8), n_rep = 5)C <- sim_met_C(q = 10, min.cor = 0.2, max.cor = 0.8, seed = 1) grid <- sim_W_grid(C, noise = c(0, 0.3, 0.6), n_var = c(20, 100), collinearity = c(0, 0.8), n_rep = 5)
Clusters soil profiles returned by get_soil into a small number
of zones using a Gaussian mixture model, selecting both the number of
clusters and the covariance structure by BIC. Returns hard cluster labels,
the full matrix of posterior membership probabilities, and a per-site
uncertainty measure.
Unlike k-means, a mixture model gives every site a probability of belonging to every zone. A site on a boundary is genuinely ambiguous, and that ambiguity is reported rather than hidden behind a hard label.
soil_classification( soil.data, env.id = "env", variables = NULL, depths = NULL, G = 1:9, model.names = NULL, scale.data = TRUE, clr.texture = TRUE, convert.units = TRUE, use.pca = FALSE, pca.var = 0.95, risk.vars = NULL, risk.direction = c("high", "low"), uncertainty.threshold = 0.7, seed = 1, verbose = TRUE )soil_classification( soil.data, env.id = "env", variables = NULL, depths = NULL, G = 1:9, model.names = NULL, scale.data = TRUE, clr.texture = TRUE, convert.units = TRUE, use.pca = FALSE, pca.var = 0.95, risk.vars = NULL, risk.direction = c("high", "low"), uncertainty.threshold = 0.7, seed = 1, verbose = TRUE )
soil.data |
data.frame. Wide-format output of
|
env.id |
character. Name of the site-id column. Default |
variables |
character. Soil properties to cluster on, for example
|
depths |
character. Depth layers to use, for example
|
G |
integer vector. Candidate numbers of clusters. Default |
model.names |
character. |
scale.data |
logical. Standardise variables to zero mean and unit
variance before fitting. Default |
clr.texture |
logical. Apply an isometric log-ratio (ilr) transform to
texture fractions (clay/sand/silt) at each depth. Default |
convert.units |
logical. Apply SoilGrids conversion factors, turning
mapped integers into conventional units. Requires |
use.pca |
logical. Reduce the feature matrix to principal components
before fitting. Default |
pca.var |
numeric in (0, 1]. Proportion of variance the retained
components must explain when |
risk.vars |
character. Variables used to order the cluster labels, for
example |
risk.direction |
|
uncertainty.threshold |
numeric in (0, 1). A site whose maximum
posterior probability falls below this is flagged |
seed |
integer or |
verbose |
logical. Print progress. Default |
What the model does. A Gaussian mixture assumes the sites are drawn
from multivariate normal components with unknown means, covariances
and mixing weights, fitted by EM. mclust additionally searches over
constrained covariance families (spherical, diagonal, ellipsoidal; equal or
varying across components), and BIC selects both and the family in
one criterion. See Fraley & Raftery (2002) and Scrucca et al. (2016).
Compositional data. Texture fractions sum to a constant. Feeding
them to a Gaussian untransformed is a category error: the closure constraint
forces spurious negative correlations. The default clr.texture = TRUE
applies an isometric log-ratio transform per depth, mapping
parts to orthonormal coordinates. A plain clr transform is
deliberately not used for fitting: its coordinates sum to zero, leaving the
covariance singular and causing BIC to select the wrong number of clusters.
Turn this off only if your variables are not compositional.
Label ordering. Mixture component labels are arbitrary and permute
between runs. Clusters are therefore renumbered by risk.vars so that
cluster 1 is always the low end and cluster the high end. Without
this, "cluster 3" would mean something different on every run and results
could not be compared or cached.
Choosing variables. A mixture estimates a mean vector and a
covariance per component, so the parameter count grows quickly with the
number of features. Passing every property at every depth gives 20+ highly
correlated columns and will overwhelm a modest number of sites. The function
warns when n < 10 * p. Prefer a handful of properties at two
contrasting depths, or set use.pca = TRUE.
Sample size. This is the single most common way to get a
meaningless result. Clustering a handful of sites produces a model in which
every site owns its own component: the output reports
max_posterior = 1 and uncertainty = 0 for every row, which
looks like total confidence and means nothing. Zone delineation samples a
grid of points across the area of interest – typically hundreds – not one
point per farm. Check against before believing any output.
Reading the BIC gap. print() reports the BIC difference
between the selected model and the runner-up, labelled weak (< 2), positive
(2-6), strong (6-10) or very strong (> 10) following the usual convention for
BIC differences. A weak gap means several partitions fit about equally well
and the zone count should not be presented as settled.
Relationship to the source paper. The paper behind this function (Zhang et al., doi:10.34133/ehs.0402) could not be read: the publisher blocks automated access. This implements the standard GMM zoning workflow its title describes – log-ratio transform, standardise, BIC-selected mixture, posterior-based uncertainty – but the paper's exact preprocessing, covariance families and thresholds are unverified. Treat this as a methodologically conventional starting point, not a reproduction.
What this cannot do. SoilGrids contains no cadmium, and no heavy
metal of any kind. The source paper's subject is cadmium risk; this function
can only cluster the properties SoilGrids actually provides (texture, organic
carbon, pH, CEC, water retention). Any "risk" interpretation rests entirely
on whichever proxies you pass to risk.vars, and the paper's machine
learning step – predicting the contaminant before zoning – has no
counterpart here. Do not present output from this function as a contaminant
risk map.
An object of class soil_gmm, a list with:
classificationdata.frame, one row per input site in input
order: env, cluster, max_posterior,
uncertainty (= 1 - max_posterior), and logical
uncertain.
posteriormatrix of posterior membership probabilities, sites x clusters, each row summing to 1.
profilesdata.frame of per-cluster means in original units,
plus n_sites.
modellist with G, modelName,
bic, loglik.
BICthe full BIC table over and covariance family.
featuresthe feature matrix actually clustered.
pcathe prcomp object, or NULL.
settingsthe call arguments, for reproducibility.
The function classifies rows, one output row per input row, in input
order. If each row is a grid point, you get a cluster per grid point; a farm
spanning several rows may well split across zones, and that split is usually
the informative result. Join back to your own table on env.
Zhang, F., Jia, Z., Wu, S., Chen, C., Chen, X., Zheng, C., Xu, M. (2025) Machine Learning and Gaussian Mixture Model for Delineating Soil Cadmium Risk Zones. Ecosystem Health and Sustainability. doi:10.34133/ehs.0402
Fraley, C. & Raftery, A.E. (2002) Model-based clustering, discriminant analysis and density estimation. Journal of the American Statistical Association 97(458), 611-631. doi:10.1198/016214502760047131
Scrucca, L., Fop, M., Murphy, T.B. & Raftery, A.E. (2016) mclust 5: clustering, classification and density estimation using Gaussian finite mixture models. The R Journal 8(1), 205-233. doi:10.32614/RJ-2016-021
Aitchison, J. (1982) The statistical analysis of compositional data. Journal of the Royal Statistical Society B 44(2), 139-177.
Egozcue, J.J., Pawlowsky-Glahn, V., Mateu-Figueras, G. & Barcelo-Vidal, C. (2003) Isometric logratio transformations for compositional data analysis. Mathematical Geology 35(3), 279-300. doi:10.1023/A:1023818214614
Schwarz, G. (1978) Estimating the dimension of a model. Annals of Statistics 6(2), 461-464. doi:10.1214/aos/1176344136
Poggio, L. et al. (2021) SoilGrids 2.0. SOIL 7, 217-240. doi:10.5194/soil-7-217-2021
get_soil for the input, water_balance
for per-site indices that need no cross-site sample,
Mclust for the underlying model.
## Not run: ## This workflow downloads SoilGrids data for a 200-site grid. library(mclust) ## --------------------------------------------------------------- ## 1. Minimal use ## --------------------------------------------------------------- soil <- get_soil(env.id = c("NM", "SO", "IP"), lat = c(-13.05, -12.32, -21.98), lon = c(-56.08, -55.71, -47.88), variables.names = c("clay", "sand", "silt", "soc", "phh2o")) fit <- soil_classification(soil) fit # summary, BIC gap, cluster profiles head(fit$classification) # per-site cluster + uncertainty ## --------------------------------------------------------------- ## 2. Realistic zoning: sample a GRID, not one point per farm ## --------------------------------------------------------------- ## A mixture cannot be identified from a handful of rows. Sample the ## area of interest, here 40 points around each of 5 locations. sites <- data.frame( env = c("SOR", "LON", "SLG", "PAL", "BAR"), lat = c(-12.5453, -23.3045, -28.4083, -10.1689, -12.1530), lon = c(-55.7113, -51.1696, -54.9608, -48.3317, -44.9900)) g <- expand.grid(k = 1:40, i = seq_len(nrow(sites))) pts <- data.frame( env = paste0(sites$env[g$i], "_", sprintf("%03d", g$k)), farm = sites$env[g$i], lat = sites$lat[g$i] + runif(nrow(g), -0.05, 0.05), lon = sites$lon[g$i] + runif(nrow(g), -0.05, 0.05)) soil_grid <- get_soil(env.id = pts$env, lat = pts$lat, lon = pts$lon, variables.names = c("clay", "sand", "silt", "soc", "phh2o")) fit <- soil_classification(soil_grid, variables = c("clay", "sand", "silt", "soc", "phh2o"), depths = c("0_5cm", "30_60cm"), risk.vars = "soc") fit ## Do farms split across zones? Usually the interesting question. table(farm = pts$farm, cluster = fit$classification$cluster) ## --------------------------------------------------------------- ## 3. Posterior probabilities and ambiguous sites ## --------------------------------------------------------------- round(head(fit$posterior), 3) # membership in EVERY zone ## Sites the model is least sure about -- these sit on a boundary. amb <- fit$classification[fit$classification$uncertain, ] amb[order(amb$max_posterior), ] ## Tighten or relax the flag without refitting the model: fit2 <- soil_classification(soil_grid, uncertainty.threshold = 0.9) sum(fit2$classification$uncertain) ## --------------------------------------------------------------- ## 4. Ordering zones so cluster 1 always means the same thing ## --------------------------------------------------------------- lo <- soil_classification(soil_grid, risk.vars = "soc", risk.direction = "high") # 1 = lowest SOC hi <- soil_classification(soil_grid, risk.vars = "soc", risk.direction = "low") # 1 = highest SOC lo$profiles[, c("cluster", "n_sites")] ## --------------------------------------------------------------- ## 5. Many correlated variables: reduce first ## --------------------------------------------------------------- fit_pca <- soil_classification(soil_grid, use.pca = TRUE, pca.var = 0.95) fit_pca$pca # how many components were kept ## Or restrict the covariance family when sites are few: fit_par <- soil_classification(soil_grid, model.names = c("EII", "VII", "EEI", "VVI")) ## --------------------------------------------------------------- ## 6. Joining results back to your own table ## --------------------------------------------------------------- out <- merge(pts, fit$classification, by = "env") head(out[, c("env", "farm", "lat", "lon", "cluster", "max_posterior", "uncertain")]) ## Dominant zone per farm, and whether the farm is homogeneous with(out, table(farm, cluster)) ## --------------------------------------------------------------- ## 7. Is the cluster count actually supported? ## --------------------------------------------------------------- fit$BIC # full table over G and family fit$model$G # selected number of zones ## print(fit) reports the gap to the runner-up; a weak gap (< 2) means ## the zone count is not settled, whatever the point estimate says. ## End(Not run)## Not run: ## This workflow downloads SoilGrids data for a 200-site grid. library(mclust) ## --------------------------------------------------------------- ## 1. Minimal use ## --------------------------------------------------------------- soil <- get_soil(env.id = c("NM", "SO", "IP"), lat = c(-13.05, -12.32, -21.98), lon = c(-56.08, -55.71, -47.88), variables.names = c("clay", "sand", "silt", "soc", "phh2o")) fit <- soil_classification(soil) fit # summary, BIC gap, cluster profiles head(fit$classification) # per-site cluster + uncertainty ## --------------------------------------------------------------- ## 2. Realistic zoning: sample a GRID, not one point per farm ## --------------------------------------------------------------- ## A mixture cannot be identified from a handful of rows. Sample the ## area of interest, here 40 points around each of 5 locations. sites <- data.frame( env = c("SOR", "LON", "SLG", "PAL", "BAR"), lat = c(-12.5453, -23.3045, -28.4083, -10.1689, -12.1530), lon = c(-55.7113, -51.1696, -54.9608, -48.3317, -44.9900)) g <- expand.grid(k = 1:40, i = seq_len(nrow(sites))) pts <- data.frame( env = paste0(sites$env[g$i], "_", sprintf("%03d", g$k)), farm = sites$env[g$i], lat = sites$lat[g$i] + runif(nrow(g), -0.05, 0.05), lon = sites$lon[g$i] + runif(nrow(g), -0.05, 0.05)) soil_grid <- get_soil(env.id = pts$env, lat = pts$lat, lon = pts$lon, variables.names = c("clay", "sand", "silt", "soc", "phh2o")) fit <- soil_classification(soil_grid, variables = c("clay", "sand", "silt", "soc", "phh2o"), depths = c("0_5cm", "30_60cm"), risk.vars = "soc") fit ## Do farms split across zones? Usually the interesting question. table(farm = pts$farm, cluster = fit$classification$cluster) ## --------------------------------------------------------------- ## 3. Posterior probabilities and ambiguous sites ## --------------------------------------------------------------- round(head(fit$posterior), 3) # membership in EVERY zone ## Sites the model is least sure about -- these sit on a boundary. amb <- fit$classification[fit$classification$uncertain, ] amb[order(amb$max_posterior), ] ## Tighten or relax the flag without refitting the model: fit2 <- soil_classification(soil_grid, uncertainty.threshold = 0.9) sum(fit2$classification$uncertain) ## --------------------------------------------------------------- ## 4. Ordering zones so cluster 1 always means the same thing ## --------------------------------------------------------------- lo <- soil_classification(soil_grid, risk.vars = "soc", risk.direction = "high") # 1 = lowest SOC hi <- soil_classification(soil_grid, risk.vars = "soc", risk.direction = "low") # 1 = highest SOC lo$profiles[, c("cluster", "n_sites")] ## --------------------------------------------------------------- ## 5. Many correlated variables: reduce first ## --------------------------------------------------------------- fit_pca <- soil_classification(soil_grid, use.pca = TRUE, pca.var = 0.95) fit_pca$pca # how many components were kept ## Or restrict the covariance family when sites are few: fit_par <- soil_classification(soil_grid, model.names = c("EII", "VII", "EEI", "VVI")) ## --------------------------------------------------------------- ## 6. Joining results back to your own table ## --------------------------------------------------------------- out <- merge(pts, fit$classification, by = "env") head(out[, c("env", "farm", "lat", "lon", "cluster", "max_posterior", "uncertain")]) ## Dominant zone per farm, and whether the farm is homogeneous with(out, table(farm, cluster)) ## --------------------------------------------------------------- ## 7. Is the cluster count actually supported? ## --------------------------------------------------------------- fit$BIC # full table over G and family fit$model$G # selected number of zones ## print(fit) reports the gap to the runner-up; a weak gap (< 2) means ## the zone count is not settled, whatever the point estimate says. ## End(Not run)
Condenses the daily output of water_balance into one row per
environment, or per environment and growth stage, producing the season-level
indices most often used as environmental covariates in
genotype-by-environment models.
summary_water_balance( wb, env.id = "env", days.id = "daysFromStart", by.interval = FALSE, time.window = NULL, names.window = NULL, verbose = TRUE )summary_water_balance( wb, env.id = "env", days.id = "daysFromStart", by.interval = FALSE, time.window = NULL, names.window = NULL, verbose = TRUE )
wb |
data.frame. Output of |
env.id |
character. Environment-id column. Default |
days.id |
character. Day counter column. Default
|
by.interval |
logical. Summarise within growth stages as well as by
environment. Default |
time.window |
numeric. Stage breakpoints in days after planting. When
|
names.window |
character. Stage names, one per interval. Ignored if the length does not match the number of intervals. |
verbose |
boolean. If |
ETa_ETc_ratio is the relative transpiration deficit, the quantity
entering most water-limited yield models – notably the FAO Ky framework of
Doorenbos & Kassam (1979), where relative yield loss is proportional to
.
max_consecutive_stress distinguishes one damaging spell from the same
number of scattered days, which a mean cannot. Ten consecutive stressed days
at flowering is a different event from ten isolated days across the season,
and yield responds very differently to the two.
Stage timing matters more than season totals. Water deficit during
flowering and grain fill costs disproportionately more yield than the same
deficit during vegetative growth (Steduto et al. 2012). Prefer
by.interval = TRUE with breakpoints matched to your crop's phenology
over a single season-long mean.
A data.frame with, per group: n_days, mean_Ks, min_Ks,
total_deficit, n_stress_days,
max_consecutive_stress, total_ETc, total_ETa,
ETa_ETc_ratio, total_rain, total_irrigation,
total_drainage, mean_ASW, mean_depletion.
Doorenbos, J. & Kassam, A.H. (1979) Yield response to water. FAO Irrigation and Drainage Paper 33. FAO, Rome.
Steduto, P., Hsiao, T.C., Fereres, E. & Raes, D. (2012) Crop yield response to water. FAO Irrigation and Drainage Paper 66. FAO, Rome.
Costa-Neto, G., Fritsche-Neto, R. & Crossa, J. (2021) Nonlinear kernels, dominance, and envirotyping data increase the accuracy of genome-based prediction in multi-environment trials. Heredity 126, 92-106. doi:10.1038/s41437-020-00353-1
water_balance, summaryWTH
## Synthetic daily weather and a uniform soil profile (mapped units). wth <- expand.grid(daysFromStart = 1:120, env = c("E1", "E2")) wth$PRECTOT <- ifelse(wth$daysFromStart %% 10 == 0, 25, 0) wth$ETP <- 5 soil <- data.frame(env = c("E1", "E2")) for (layer in c("0_5cm", "5_15cm", "15_30cm", "30_60cm", "60_100cm")) { soil[[paste0("wv0033|", layer)]] <- 300 soil[[paste0("wv1500|", layer)]] <- 150 } wb <- water_balance(wth, soil) ## Season-level, one row per environment summary_water_balance(wb) ## By maize growth stage summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120), names.window = c("P-E", "E-V1", "V1-V4", "V4-VT", "VT-GF", "GF-PM")) ## Default 10-day windows when no breakpoints are given summary_water_balance(wb, by.interval = TRUE) ## Rank environments by flowering-period stress s <- summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 60, 90, 120), names.window = c("veg", "flower", "fill")) s[s$interval == "flower", ][order(s$mean_Ks[s$interval == "flower"]), ]## Synthetic daily weather and a uniform soil profile (mapped units). wth <- expand.grid(daysFromStart = 1:120, env = c("E1", "E2")) wth$PRECTOT <- ifelse(wth$daysFromStart %% 10 == 0, 25, 0) wth$ETP <- 5 soil <- data.frame(env = c("E1", "E2")) for (layer in c("0_5cm", "5_15cm", "15_30cm", "30_60cm", "60_100cm")) { soil[[paste0("wv0033|", layer)]] <- 300 soil[[paste0("wv1500|", layer)]] <- 150 } wb <- water_balance(wth, soil) ## Season-level, one row per environment summary_water_balance(wb) ## By maize growth stage summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120), names.window = c("P-E", "E-V1", "V1-V4", "V4-VT", "VT-GF", "GF-PM")) ## Default 10-day windows when no breakpoints are given summary_water_balance(wb, by.interval = TRUE) ## Rank environments by flowering-period stress s <- summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 60, 90, 120), names.window = c("veg", "flower", "fill")) s[s$interval == "flower", ][order(s$mean_Ks[s$interval == "flower"]), ]
Summarises a get_weather output by environment and, optionally, by
user-defined time intervals such as phenological stages.
summaryWTH( env.data, id.names = NULL, env.id = NULL, days.id = NULL, var.id = NULL, statistic = NULL, probs = NULL, by.interval = FALSE, time.window = NULL, names.window = NULL, verbose = TRUE )summaryWTH( env.data, id.names = NULL, env.id = NULL, days.id = NULL, var.id = NULL, statistic = NULL, probs = NULL, by.interval = FALSE, time.window = NULL, names.window = NULL, verbose = TRUE )
env.data |
data.frame. A |
id.names |
vector (character). Columns treated as identifiers. |
env.id |
character. Environment-id column. Default |
days.id |
character. Column holding days from start. Default
|
var.id |
vector (character). Variables to summarise. Defaults to every non-identifier column. |
statistic |
character. One of |
probs |
vector (numeric). Quantile probabilities. Default
|
by.interval |
boolean. Summarise within time intervals. |
time.window |
vector (numeric). Interval breakpoints in days. |
names.window |
vector (character). Interval names. |
verbose |
boolean. If |
Quantiles are computed with stats::aggregate rather than the previous
nested foreach loops, which registered no parallel backend and therefore
ran sequentially while emitting a warning on every call.
A data.frame in long format with one row per environment (and interval) and variable.
Germano Costa Neto
get_weather, processWTH
env.data <- get_weather(lat = -13.05, lon = -56.05) summaryWTH(env.data, env.id = "env", statistic = "mean") summaryWTH(env.data, env.id = "env", by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120), names.window = c("P-E", "E-V1", "V1-V4", "V4-VT", "VT-GF", "GF-PM"))env.data <- get_weather(lat = -13.05, lon = -56.05) summaryWTH(env.data, env.id = "env", statistic = "mean") summaryWTH(env.data, env.id = "env", by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120), names.window = c("P-E", "E-V1", "V1-V4", "V4-VT", "VT-GF", "GF-PM"))
The envirotype counterpart of W_matrix. Runs
env_typing and returns, unconditionally, an
environment x envirotype numeric matrix suitable for env_kernel
and env_cluster – regardless of which of the four
env_typing() branches ran. Where W_matrix summarises continuous
covariables, T_matrix summarises the frequencies of discrete
environmental types (envirotypes), the "enviromic assembly" building block of
Costa-Neto, Crossa & Fritsche-Neto (2021).
T_matrix( env.data, var.id = NULL, env.id = NULL, cardinals = NULL, days.id = NULL, time.window = NULL, names.window = NULL, quantiles = NULL, id.names = NULL, by.interval = FALSE, joint = FALSE, joint.by.interval = TRUE, envirotype_mining = FALSE, k.range = 2:10, nstart = 25, iter.max = 100, seed = 1234, combine.features = FALSE, combine.max.order = 2L, ratio = TRUE, center = FALSE, scale = FALSE, sd.tol = 10, tol = 0.001, QC = FALSE, impute = c("none", "mean", "drop"), drop.constant = TRUE, verbose = TRUE )T_matrix( env.data, var.id = NULL, env.id = NULL, cardinals = NULL, days.id = NULL, time.window = NULL, names.window = NULL, quantiles = NULL, id.names = NULL, by.interval = FALSE, joint = FALSE, joint.by.interval = TRUE, envirotype_mining = FALSE, k.range = 2:10, nstart = 25, iter.max = 100, seed = 1234, combine.features = FALSE, combine.max.order = 2L, ratio = TRUE, center = FALSE, scale = FALSE, sd.tol = 10, tol = 0.001, QC = FALSE, impute = c("none", "mean", "drop"), drop.constant = TRUE, verbose = TRUE )
env.data |
data.frame of processed weather data, or an object already
returned by |
var.id, env.id, cardinals, days.id, time.window, names.window, quantiles
|
Passed to |
id.names, by.interval, joint, joint.by.interval
|
Passed to
|
envirotype_mining, k.range, nstart, iter.max, seed
|
Passed to
|
combine.features, combine.max.order
|
Passed to |
ratio |
logical. Convert counts to within-environment relative
frequencies. Default |
center, scale
|
logical. Passed to the shared scaler. Both default
to |
sd.tol, tol, QC, impute
|
Passed to the shared scaler. |
drop.constant |
logical. Drop envirotype columns that are constant across
environments (they contribute nothing to a kernel). Default |
verbose |
logical. |
Why ratio = TRUE by default. Raw envirotype counts are days,
so an environment with a longer season has larger counts in every column. A
kernel built on raw counts is then partly a kernel on season length.
Row-normalising makes each environment a composition – the share of its
season spent in each envirotype – which is what "envirotype frequency" is
meant to convey.
Why center = scale = FALSE by default. This deliberately
differs from W_matrix, where center = scale = TRUE. After
ratio = TRUE the columns are already on a common [0, 1] scale and each
row sums to 1. Scaling a composition to unit variance per column inflates rare
envirotypes – a type present in 2% of days in one environment and 0%
elsewhere becomes a high-leverage column. If you want the W_matrix
convention, set them explicitly; the choice is yours but it should be a choice.
Compositional caveat. With ratio = TRUE the rows sum to 1, so
the columns are linearly dependent and the matrix is rank-deficient by exactly
one. This is harmless for env_kernel (a Gaussian/linear kernel is
still positive semi-definite) but will produce one zero eigenvalue. Do not
"fix" it by dropping a column – that makes the result depend on which column
you dropped.
A numeric matrix of class c("T_matrix", "matrix", "array"),
with rownames = environments and colnames = envirotypes.
Attributes: "typologies" (the long frame),
"envirotype_description", "mining_summary", "plot",
"ratio", "dropped", and the .env_w_scale() attributes
when scaling is requested.
Costa-Neto, G., Crossa, J., & Fritsche-Neto, R. (2021). Enviromic assembly increases accuracy and reduces costs of the genomic prediction for yield plasticity in maize. Frontiers in Plant Science 12, 717552.
W_matrix for the covariable counterpart,
env_typing for the underlying typology,
env_kernel and env_cluster for the consumers.
data(maizeWTH) Tm <- T_matrix(maizeWTH, var.id = c("T2M", "PRECTOT"), env.id = "env") dim(Tm) K <- env_kernel(env.data = Tm, is.scaled = TRUE)data(maizeWTH) Tm <- T_matrix(maizeWTH, var.id = c("T2M", "PRECTOT"), env.id = "env") dim(Tm) K <- env_kernel(env.data = Tm, is.scaled = TRUE)
Converts an env_risk_profile() into per-environment-type weights
summing to one, so that selection indices, weighted GxE models and testing
network decisions can be made proportional to how often each environment
type actually occurs – rather than how often it happened to be trialled.
tpe_weights( risk, stage = NULL, var = NULL, by = c("class", "site"), measure = c("freq", "days"), drop.benign = FALSE, verbose = TRUE )tpe_weights( risk, stage = NULL, var = NULL, by = c("class", "site"), measure = c("freq", "days"), drop.benign = FALSE, verbose = TRUE )
risk |
An |
stage |
character. Restrict to one stage (e.g. |
var |
character. Restrict to one variable. If |
by |
character. |
measure |
character. |
drop.benign |
boolean. Exclude classes named |
verbose |
boolean. Print the weight table. |
What "site weight" means. With by = "site" the weight is the
share of the TPE's total stress-occurrence mass that a site contributes. A
high weight means the site frequently expresses the stresses of interest,
NOT that it is a good discriminator of genotypes – those are different
criteria, and a site can be highly representative yet have poor
heritability. Combine these weights with per-site repeatability before
cutting a testing location.
Weights are as good as the record. They inherit every sampling limitation of the underlying profile, and they assume the historical climatology still holds.
A data.frame with the grouping column, the raw measure, and
weight summing to 1. Attribute "measure" records which
measure was used.
## Synthetic daily stress indices for two sites over three seasons. wth <- expand.grid(day = 0:149, year = 2021:2023, env = c("E1", "E2")) wth$YYYYMMDD <- as.Date(paste0(wth$year, "-01-01")) + wth$day wth$PETP <- ifelse(wth$env == "E1", 0.4, 0.8) + 0.2 * sin(wth$day / 10) rp <- env_risk_profile(wth, var.id = "PETP", by.stage = FALSE) ## How much of the TPE is each drought class over the season? tpe_weights(rp, var = "PETP") ## Which sites carry the most of the TPE's stress mass? tpe_weights(rp, var = "PETP", by = "site", drop.benign = TRUE)## Synthetic daily stress indices for two sites over three seasons. wth <- expand.grid(day = 0:149, year = 2021:2023, env = c("E1", "E2")) wth$YYYYMMDD <- as.Date(paste0(wth$year, "-01-01")) + wth$day wth$PETP <- ifelse(wth$env == "E1", 0.4, 0.8) + 0.2 * sin(wth$day / 10) rp <- env_risk_profile(wth, var.id = "PETP", by.stage = FALSE) ## How much of the TPE is each drought class over the season? tpe_weights(rp, var = "PETP") ## Which sites carry the most of the TPE's stress mass? tpe_weights(rp, var = "PETP", by = "site", drop.benign = TRUE)
The GxE kernel has a flat eigenvalue tail that behaves like an identity matrix and competes with the residual. Dropping it reduces sigma2_GE inflation (mean abs bias 0.333 -> 0.130) at essentially no cost in cross-validated prediction (CV2 0.678 full vs 0.669 truncated).
truncate_gxe_kernel(random, keep_prop = 0.75, pattern = "GE")truncate_gxe_kernel(random, keep_prop = 0.75, pattern = "GE")
random |
a named list of kernels (the |
keep_prop |
numeric in (0, 1]. Proportion of the GxE eigenvalue mass to retain. Default 0.75. |
pattern |
character. Regular expression selecting the GxE kernels to truncate.
Default |
CAUTION: truncation distorts the G:GxE ratio – rg moved to 0.72 vs a true 0.595 in testing. Use only when sigma2_GE specifically matters.
Any cached decomposition is dropped, since the kernel changes.
The input random list of kernels, with the selected GxE
kernels replaced by their reduced-rank (truncated) version and any cached
spectral decomposition removed so the kernels are re-decomposed on the next
fit. When no kernel matches pattern, or keep_prop >= 1, the
list is returned unchanged.
Use after modifying a kernel in place, to force recomputation.
undecompose_kernels(K)undecompose_kernels(K)
K |
a kernel list from |
K with decompositions and metadata removed.
var_total = var(y) (PHENOTYPIC; var(yHat) carries no residual and yields a NEGATIVE residual by subtraction). var_resid = var_total - (varG + varE + varGE) h2_entry = varG / (varG + varE/q + varGE/(p*q) + var_resid) varG is retained in the denominator; omitting it produced h2 > 1 even with true inputs (observed 1.025 and 0.983).
varcomp_summary( fit, method = c("postmean", "gibbs"), y = NULL, p = NULL, q = NULL, digits = 4 )varcomp_summary( fit, method = c("postmean", "gibbs"), y = NULL, p = NULL, q = NULL, digits = 4 )
fit |
a fitted |
method |
character. |
y |
numeric. Observed phenotypes used as the total-variance reference.
Taken from |
p |
integer. Number of genotypes. Inferred from |
q |
integer. Number of environments. Inferred from |
digits |
integer. Number of decimal places in the summary table. Default 4. |
An object of class "km_varcomp", a list with elements
table (a data.frame of variance components with their absolute
values and proportions of the phenotypic variance), the individual
variances varG, varE, varGE, var_resid and
var_total, the heritabilities h2_entry_mean and
h2_plot, the genetic correlation rg, the counts p
and q, the method used, and the logical flags
negative_residual and few_environments. A dedicated
print method displays the table and derived quantities.
Estimates a weather covariable realized matrix W with dimensions
, for environments and covariables. This matrix is
the basic input of the reaction-norm and enviromic-assisted models in
EnvRtype (env_kernel, env_cluster,
env_target_importance, get_kernel).
W_matrix( env.data, is.processed = FALSE, id.names = NULL, env.id = NULL, var.id = NULL, probs = NULL, by.interval = NULL, time.window = NULL, names.window = NULL, center = TRUE, scale = TRUE, sd.tol = 10, statistic = NULL, tol = 0.001, QC = FALSE, impute = c("none", "mean", "median", "knn", "drop"), knn = 5L, max.cor = NULL, group = FALSE, cor.report = NULL, verbose = TRUE, copula = NULL, copula.args = list() )W_matrix( env.data, is.processed = FALSE, id.names = NULL, env.id = NULL, var.id = NULL, probs = NULL, by.interval = NULL, time.window = NULL, names.window = NULL, center = TRUE, scale = TRUE, sd.tol = 10, statistic = NULL, tol = 0.001, QC = FALSE, impute = c("none", "mean", "median", "knn", "drop"), knn = 5L, max.cor = NULL, group = FALSE, cor.report = NULL, verbose = TRUE, copula = NULL, copula.args = list() )
env.data |
data.frame of environmental variables from |
is.processed |
boolean. Indicates whether the data.frame was previously processed
with |
id.names |
character. Columns used as id for the environmental variables. |
env.id |
character. Column used as id for environments. |
var.id |
vector (character). Which variables will be used in the analysis. |
probs |
vector (numeric). Probability quantiles in |
by.interval |
boolean. Indicates if temporal intervals must be computed inside
each environment. Default |
time.window |
vector (numeric). If |
names.window |
vector (character). If |
center |
boolean. Indicates whether the matrix should be centred. Default |
scale |
boolean. If |
sd.tol |
numeric. Maximum standard deviation tolerated in quality control. Default 10. |
statistic |
vector (character). Statistic to be computed,
|
tol |
numeric. Numerical tolerance; variables whose standard deviation is at or
below |
QC |
boolean. Indicates whether Quality Control is applied. QC removes variables
with |
impute |
character. How to handle missing values before scaling: |
knn |
integer. Number of nearest environments used when |
max.cor |
numeric in |
group |
boolean. Only used when |
cor.report |
|
verbose |
boolean. If |
copula |
character or NULL. If not |
copula.args |
list. Extra arguments passed to |
Quality control follows Morais Junior et al. (2018): covariables whose standard deviation
across environments exceeds sd.tol are discarded, as are near-constant covariables
() which carry no information about environmental differences.
Three further, independent cleaning steps are available. Missing values can be imputed
column-wise (impute = "mean"/"median") or from the most similar environments
(impute = "knn") before any statistic is computed, which avoids silently shrinking
the covariable set the way impute = "drop" does. Redundant covariables can be pruned
with max.cor: enviromic matrices frequently contain near-duplicated columns (e.g.
several temperature summaries), which inflate Euclidean geometry and relatedness kernels;
the greedy filter keeps one representative per correlated block.
Unlike earlier versions, the numerical tolerance is not added to the data before scaling (which silently shifted unscaled outputs); it is used only to detect near-constant columns.
An environmental covariable realized matrix with dimensions .
The centring and scaling values, plus the list of removed markers, are attached as
attributes ("scaled:center", "scaled:scale", "removed") so that new
environments can be projected onto the same space. When quality control or collinearity
filtering drop covariables, the reason for each removal is reported in the
"removed.reason" attribute (a named character vector with values
"too.variable", "near.constant" or "collinear"). The full
collinearity diagnosis is attached as the "collinearity" data.frame attribute, and
when group = TRUE the block-to-member mapping is attached as "groups".
Germano Costa Neto
Costa-Neto G., Galli G., Carvalho H.F., Crossa J., Fritsche-Neto R. (2021). EnvRtype: a software to interplay enviromics and quantitative genomics in agriculture. G3 11(4), jkab040.
summaryWTH, env_kernel, env_typing,
T_matrix
data("maizeWTH") env.data <- maizeWTH[maizeWTH$daysFromStart < 100, ] ## Mean-centred and scaled matrix (default statistic = 'mean') W <- W_matrix(env.data = env.data) dim(W) ## Adding time windows (one block of covariables per development stage) W <- W_matrix(env.data = env.data, by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120)) ## Selecting the statistic to be used W <- W_matrix(env.data = env.data, by.interval = TRUE, statistic = 'quantile', time.window = c(0, 14, 35, 60, 90, 120)) ## With Quality Control based on the maximum sd tolerated W <- W_matrix(env.data = env.data, QC = TRUE, sd.tol = 3) attr(W, "removed") ## Dropping redundant (collinear) covariables and imputing missing cells W <- W_matrix(env.data = env.data, impute = "knn", max.cor = 0.95) attr(W, "removed.reason") ## Grouping collinear covariables into composite blocks + CSV diagnosis W <- W_matrix(env.data = env.data, max.cor = 0.9, group = TRUE, cor.report = tempdir()) attr(W, "groups") ## Creating W for specific variables W <- W_matrix(env.data = env.data, var.id = c('T2M_MAX', 'T2M_MIN', 'T2M')) ## Combining with summaryWTH by using is.processed = TRUE data <- summaryWTH(env.data, env.id = 'env', statistic = 'quantile') W <- W_matrix(env.data = data, is.processed = TRUE)data("maizeWTH") env.data <- maizeWTH[maizeWTH$daysFromStart < 100, ] ## Mean-centred and scaled matrix (default statistic = 'mean') W <- W_matrix(env.data = env.data) dim(W) ## Adding time windows (one block of covariables per development stage) W <- W_matrix(env.data = env.data, by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120)) ## Selecting the statistic to be used W <- W_matrix(env.data = env.data, by.interval = TRUE, statistic = 'quantile', time.window = c(0, 14, 35, 60, 90, 120)) ## With Quality Control based on the maximum sd tolerated W <- W_matrix(env.data = env.data, QC = TRUE, sd.tol = 3) attr(W, "removed") ## Dropping redundant (collinear) covariables and imputing missing cells W <- W_matrix(env.data = env.data, impute = "knn", max.cor = 0.95) attr(W, "removed.reason") ## Grouping collinear covariables into composite blocks + CSV diagnosis W <- W_matrix(env.data = env.data, max.cor = 0.9, group = TRUE, cor.report = tempdir()) attr(W, "groups") ## Creating W for specific variables W <- W_matrix(env.data = env.data, var.id = c('T2M_MAX', 'T2M_MIN', 'T2M')) ## Combining with summaryWTH by using is.processed = TRUE data <- summaryWTH(env.data, env.id = 'env', statistic = 'quantile') W <- W_matrix(env.data = data, is.processed = TRUE)
Runs a FAO-56 single crop coefficient soil water balance by combining daily
weather from get_weather with soil hydraulic properties from
get_soil, returning daily root-zone depletion, a water-stress
coefficient, actual evapotranspiration, and a categorical stress level.
The stress coefficient Ks is the main output: bounded in [0, 1],
dimensionless, and directly proportional to the reduction in transpiration.
water_balance( env.data, soil.data, env.id = "env", days.id = "daysFromStart", PREC = "PRECTOT", ETo = NULL, irrigation = 0, root.depth = 1000, root.init = 300, dap.root.max = 60, kc.stages = c(1, 20, 45, 90, 120), kc.values = c(0.4, 0.75, 1.15, 1.15, 0.6), p = 0.55, p.adjust = TRUE, initial.depletion = NULL, fc.var = "wv0033", pwp.var = "wv1500", convert.units = TRUE, verbose = TRUE )water_balance( env.data, soil.data, env.id = "env", days.id = "daysFromStart", PREC = "PRECTOT", ETo = NULL, irrigation = 0, root.depth = 1000, root.init = 300, dap.root.max = 60, kc.stages = c(1, 20, 45, 90, 120), kc.values = c(0.4, 0.75, 1.15, 1.15, 0.6), p = 0.55, p.adjust = TRUE, initial.depletion = NULL, fc.var = "wv0033", pwp.var = "wv1500", convert.units = TRUE, verbose = TRUE )
env.data |
data.frame. Daily weather, typically from
|
soil.data |
data.frame. Soil properties from
|
env.id |
character. Environment-id column present in both inputs.
Default |
days.id |
character. Column giving days from planting. Default
|
PREC |
character. Precipitation column (mm/day). Default
|
ETo |
character. Reference evapotranspiration column (mm/day). If
|
irrigation |
numeric or character. Irrigation in mm/day: a single value
applied daily, a vector as long as |
root.depth |
numeric. Maximum rooting depth in mm. Default 1000. |
root.init |
numeric. Rooting depth at emergence in mm. Default 300. |
dap.root.max |
numeric. Days after planting at which maximum rooting depth is reached. Default 60. |
kc.stages, kc.values
|
numeric. Days after planting and the corresponding crop coefficients, linearly interpolated between. Defaults describe a generic 120-day maize crop (FAO-56 Table 12). Calibrate these. |
p |
numeric. Soil-water depletion fraction for no stress (FAO-56 Table 22). Default 0.55 for maize. |
p.adjust |
logical. Adjust |
initial.depletion |
numeric. Root-zone depletion at planting in mm, or
|
fc.var, pwp.var
|
character. SoilGrids property names for field capacity
and wilting point. Defaults |
convert.units |
logical. Apply ISRIC conversion factors to the soil
data. Set |
verbose |
logical. Print progress messages. Default |
Method. FAO-56 single crop coefficient approach (Allen et al. 1998, Ch. 8). Root-zone depletion is stepped forward daily:
bounded to . Water in excess of field capacity becomes deep
percolation; depletion cannot exceed total available water.
The stress coefficient follows FAO-56 Eq. 84:
so transpiration proceeds at the potential rate until readily available water is exhausted, then declines linearly to zero at wilting point.
Causal ordering. Ks on day is computed from
yesterday's depletion against today's capacity. Using the same
day's closing depletion would let today's rainfall relieve stress that
already reduced today's transpiration – a subtle look-ahead that
systematically understates stress.
Units. SoilGrids serves integers in mapped units:
wv0033 = 314 means 0.314 cm3/cm3. Conversion is automatic; see
convert.units.
Total available water is integrated across the SoilGrids layers intersecting the root zone, each weighted by the depth it contributes, not taken from a single layer. Because rooting depth grows over the season, TAW is recomputed daily (cached by unique depth for speed).
What this does not model. No dual crop coefficient, so soil evaporation is not separated from transpiration. No capillary rise, no lateral flow, and no surface runoff – all rainfall is assumed to infiltrate, which overestimates stored water and understates stress on steep, crusted or low-infiltration soils. Subtract runoff from the precipitation column beforehand if that matters. Irrigation is applied as given; the balance does not schedule it.
Interpreting the output. Regress Ks against yield: it is
bounded, dimensionless, and scales transpiration directly.
depletion_frac is easier to explain to agronomists.
stress_level is a convenience for grouping and tabulation and should
not be used as a modelling covariate – it discards magnitude.
A data.frame with one row per environment-day, containing the input columns plus:
TAWtotal available water in the root zone (mm)
RAWreadily available water, (mm)
Drroot-zone depletion at end of day (mm)
ASWavailable soil water remaining, (mm)
depletion_frac; 0 at field capacity, 1 at
wilting point
Kswater-stress coefficient; 1 unstressed, 0 fully stressed
ETccrop evapotranspiration, standard conditions (mm/day)
ETaactual evapotranspiration,
deficit (mm/day)
drainagedeep percolation below the root zone (mm/day)
irrigationirrigation applied (mm/day)
Kcinterpolated crop coefficient
root_depthrooting depth that day (mm)
stress_levelordered factor: none, mild, moderate, severe
Allen, R.G., Pereira, L.S., Raes, D. & Smith, M. (1998) Crop evapotranspiration: guidelines for computing crop water requirements. FAO Irrigation and Drainage Paper 56. FAO, Rome.
Pereira, L.S. et al. (2021) Standard single and basal crop coefficients for field crops. Agricultural Water Management 243, 106466. doi:10.1016/j.agwat.2020.106466
Doorenbos, J. & Kassam, A.H. (1979) Yield response to water. FAO Irrigation and Drainage Paper 33. FAO, Rome.
Steduto, P., Hsiao, T.C., Fereres, E. & Raes, D. (2012) Crop yield response to water. FAO Irrigation and Drainage Paper 66. FAO, Rome.
Poggio, L. et al. (2021) SoilGrids 2.0. SOIL 7, 217-240. doi:10.5194/soil-7-217-2021
Turek, M.E. et al. (2023) Global mapping of volumetric water retention at 100, 330 and 15000 cm suction using the WoSIS database. International Soil and Water Conservation Research 11(2), 225-239. doi:10.1016/j.iswcr.2022.08.001
Borg, H. & Grimes, D.W. (1986) Depth development of roots with time. Transactions of the ASAE 29(1), 194-197. doi:10.13031/2013.30125
summary_water_balance to condense the daily output,
get_weather and get_soil for the inputs,
processWTH to derive ETo,
soil_classification for cross-site soil zoning.
## Not run: ## Requires live NASA POWER and SoilGrids services. ## --------------------------------------------------------------- ## 1. Minimal use ## --------------------------------------------------------------- wth <- get_weather(env.id = "NM", lat = -13.05, lon = -56.05, start.day = "2015-02-15", end.day = "2015-06-15") wth <- processWTH(wth) # adds ETP soil <- get_soil(env.id = "NM", lat = -13.05, lon = -56.05, variables.names = c("wv0033", "wv1500", "clay")) wb <- water_balance(wth, soil) head(wb[, c("env", "daysFromStart", "TAW", "Dr", "Ks", "deficit", "stress_level")]) ## --------------------------------------------------------------- ## 2. Several environments at once ## --------------------------------------------------------------- sites <- data.frame( env = c("SOR", "LON", "BAR"), lat = c(-12.5453, -23.3045, -12.1530), lon = c(-55.7113, -51.1696, -44.9900)) wth <- processWTH(get_weather(env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "2023-10-15", end.day = "2024-02-12")) soil <- get_soil(env.id = sites$env, lat = sites$lat, lon = sites$lon, variables.names = c("wv0033", "wv1500")) wb <- water_balance(wth, soil, root.depth = 1000) summary_water_balance(wb) ## --------------------------------------------------------------- ## 3. Irrigation: constant, per-day, or from a column ## --------------------------------------------------------------- water_balance(wth, soil, irrigation = 5) # 5 mm every day water_balance(wth, soil, irrigation = rep(0, nrow(wth))) wth$irr <- ifelse(wth$daysFromStart %in% 40:70, 8, 0) # top-up at flowering wb_irr <- water_balance(wth, soil, irrigation = "irr") ## How much stress did irrigation remove? c(rainfed = sum(water_balance(wth, soil, verbose = FALSE)$deficit), irrigated = sum(wb_irr$deficit)) ## --------------------------------------------------------------- ## 4. A different crop: soybean ## --------------------------------------------------------------- ## Kc and p come from FAO-56 Tables 12 and 22. Always check them ## against your own crop, cycle length and region. wb_soy <- water_balance( wth, soil, kc.stages = c(1, 25, 50, 95, 120), kc.values = c(0.40, 0.80, 1.15, 1.15, 0.50), p = 0.50, # soybean, FAO-56 Table 22 root.depth = 800) ## --------------------------------------------------------------- ## 5. Restricted root zone (compaction, shallow profile) ## --------------------------------------------------------------- shallow <- water_balance(wth, soil, root.depth = 400, verbose = FALSE) deep <- water_balance(wth, soil, root.depth = 1200, verbose = FALSE) c(shallow_TAW = max(shallow$TAW), deep_TAW = max(deep$TAW)) ## A shallower profile stores less water and stresses sooner. ## --------------------------------------------------------------- ## 6. Dry planting: start part-depleted rather than at field capacity ## --------------------------------------------------------------- wb_dry <- water_balance(wth, soil, initial.depletion = 60) ## --------------------------------------------------------------- ## 7. Season and growth-stage summaries ## --------------------------------------------------------------- summary_water_balance(wb) summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120), names.window = c("P-E", "E-V1", "V1-V4", "V4-VT", "VT-GF", "GF-PM")) ## Stress at VT-GF (flowering to grain fill) costs far more yield ## than the same stress during vegetative growth. ## --------------------------------------------------------------- ## 8. Using the indices as envirotype covariates ## --------------------------------------------------------------- W <- summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 30, 60, 90, 120)) wide <- reshape(W[, c("env", "interval", "mean_Ks")], idvar = "env", timevar = "interval", direction = "wide") head(wide) # one row per environment, ready to join to trial data ## End(Not run)## Not run: ## Requires live NASA POWER and SoilGrids services. ## --------------------------------------------------------------- ## 1. Minimal use ## --------------------------------------------------------------- wth <- get_weather(env.id = "NM", lat = -13.05, lon = -56.05, start.day = "2015-02-15", end.day = "2015-06-15") wth <- processWTH(wth) # adds ETP soil <- get_soil(env.id = "NM", lat = -13.05, lon = -56.05, variables.names = c("wv0033", "wv1500", "clay")) wb <- water_balance(wth, soil) head(wb[, c("env", "daysFromStart", "TAW", "Dr", "Ks", "deficit", "stress_level")]) ## --------------------------------------------------------------- ## 2. Several environments at once ## --------------------------------------------------------------- sites <- data.frame( env = c("SOR", "LON", "BAR"), lat = c(-12.5453, -23.3045, -12.1530), lon = c(-55.7113, -51.1696, -44.9900)) wth <- processWTH(get_weather(env.id = sites$env, lat = sites$lat, lon = sites$lon, start.day = "2023-10-15", end.day = "2024-02-12")) soil <- get_soil(env.id = sites$env, lat = sites$lat, lon = sites$lon, variables.names = c("wv0033", "wv1500")) wb <- water_balance(wth, soil, root.depth = 1000) summary_water_balance(wb) ## --------------------------------------------------------------- ## 3. Irrigation: constant, per-day, or from a column ## --------------------------------------------------------------- water_balance(wth, soil, irrigation = 5) # 5 mm every day water_balance(wth, soil, irrigation = rep(0, nrow(wth))) wth$irr <- ifelse(wth$daysFromStart %in% 40:70, 8, 0) # top-up at flowering wb_irr <- water_balance(wth, soil, irrigation = "irr") ## How much stress did irrigation remove? c(rainfed = sum(water_balance(wth, soil, verbose = FALSE)$deficit), irrigated = sum(wb_irr$deficit)) ## --------------------------------------------------------------- ## 4. A different crop: soybean ## --------------------------------------------------------------- ## Kc and p come from FAO-56 Tables 12 and 22. Always check them ## against your own crop, cycle length and region. wb_soy <- water_balance( wth, soil, kc.stages = c(1, 25, 50, 95, 120), kc.values = c(0.40, 0.80, 1.15, 1.15, 0.50), p = 0.50, # soybean, FAO-56 Table 22 root.depth = 800) ## --------------------------------------------------------------- ## 5. Restricted root zone (compaction, shallow profile) ## --------------------------------------------------------------- shallow <- water_balance(wth, soil, root.depth = 400, verbose = FALSE) deep <- water_balance(wth, soil, root.depth = 1200, verbose = FALSE) c(shallow_TAW = max(shallow$TAW), deep_TAW = max(deep$TAW)) ## A shallower profile stores less water and stresses sooner. ## --------------------------------------------------------------- ## 6. Dry planting: start part-depleted rather than at field capacity ## --------------------------------------------------------------- wb_dry <- water_balance(wth, soil, initial.depletion = 60) ## --------------------------------------------------------------- ## 7. Season and growth-stage summaries ## --------------------------------------------------------------- summary_water_balance(wb) summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 14, 35, 60, 90, 120), names.window = c("P-E", "E-V1", "V1-V4", "V4-VT", "VT-GF", "GF-PM")) ## Stress at VT-GF (flowering to grain fill) costs far more yield ## than the same stress during vegetative growth. ## --------------------------------------------------------------- ## 8. Using the indices as envirotype covariates ## --------------------------------------------------------------- W <- summary_water_balance(wb, by.interval = TRUE, time.window = c(0, 30, 60, 90, 120)) wide <- reshape(W[, c("env", "interval", "mean_Ks")], idvar = "env", timevar = "interval", direction = "wide") head(wide) # one row per environment, ready to join to trial data ## End(Not run)