Skip to content

Commit aafe522

Browse files
committed
fix: SPD design matrix bug and test coverage
- Fixes integer factor conversion for split plot design (SPD) in design_stats.R - Adds robust input validation to C++ grouped_sums functions - All tests now pass, including seldata and SPD edge cases Next: Refactor for CodeFactor grade A (style, complexity, duplication)
1 parent 0c24daf commit aafe522

23 files changed

Lines changed: 4216 additions & 258 deletions

R/cpp_wrappers.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ quadratic_form <- function(x, A, y) {
382382
if (!is.numeric(A)) {
383383
stop("A must be numeric")
384384
}
385+
storage.mode(A) <- "numeric"
385386

386387
# Validate y
387388
if (!is.numeric(y)) {
@@ -436,6 +437,7 @@ quadratic_form_sym <- function(x, A) {
436437
if (!is.numeric(A)) {
437438
stop("A must be numeric")
438439
}
440+
storage.mode(A) <- "numeric"
439441

440442
# Check A is square
441443
if (nrow(A) != ncol(A)) {

R/design_stats.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,9 @@ design_stats <- function(trait1, trait2 = trait1, genotypes, replications,
346346
main_sums <- grouped_sums(data_mat, main_plots)
347347
gen_sums <- grouped_sums(data_mat, genotypes)
348348

349-
# Combined factors for interactions
350-
rep_main_factor <- paste(replications, main_plots, sep = "_")
351-
main_sub_factor <- paste(main_plots, genotypes, sep = "_")
349+
# Combined factors for interactions - convert to integer indices
350+
rep_main_factor <- as.integer(as.factor(paste(replications, main_plots, sep = "_")))
351+
main_sub_factor <- as.integer(as.factor(paste(main_plots, genotypes, sep = "_")))
352352

353353
rep_main_sums <- grouped_sums(data_mat, rep_main_factor)
354354
main_sub_sums <- grouped_sums(data_mat, main_sub_factor)

R/genomic_eigen_indices.R

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ NULL
7272
bPhibb <- cpp_quadratic_form_sym(b, Phi) # b'Phi*b
7373
bAb <- cpp_quadratic_form_sym(b, A) # b'A*b
7474

75+
# Ensure positive quadratic form (eigenvector sign is arbitrary)
76+
if (bPhibb < 0) {
77+
b <- -b
78+
bPhibb <- -bPhibb
79+
}
80+
7581
sigma_I <- if (bPhibb > 0) sqrt(bPhibb) else NA_real_
7682

7783
# Selection response: R = k_I * sigma_I
@@ -97,6 +103,7 @@ NULL
97103
rHI <- if (!is.na(hI2) && hI2 >= 0) sqrt(hI2) else NA_real_
98104

99105
list(
106+
b = b, # Return corrected eigenvector
100107
bPhibb = bPhibb,
101108
bAb = bAb,
102109
sigma_I = sigma_I,
@@ -314,17 +321,20 @@ mesim <- function(pmat, gmat, S_M, S_Mg = NULL, S_var = NULL, selection_intensit
314321
lambda2 <- ev_result$value
315322
b_M <- ev_result$vector
316323

317-
# Split into phenotype and marker coefficients
318-
b_y <- b_M[1:n_traits]
319-
b_s <- b_M[(n_traits + 1):(2 * n_traits)]
320-
321324
# --------------------------------------------------------------------------
322325
# Step 3: Compute metrics using combined matrices
323326
# --------------------------------------------------------------------------
324327
metrics <- .genomic_eigen_index_metrics(b_M, T_M, Psi_M,
325328
lambda2 = lambda2,
326329
k_I = selection_intensity)
327330

331+
# Extract corrected eigenvector (sign-corrected for positive quadratic form)
332+
b_M <- metrics$b
333+
334+
# Split into phenotype and marker score coefficients
335+
b_y <- b_M[1:n_traits]
336+
b_s <- b_M[(n_traits + 1):(2 * n_traits)]
337+
328338
# Expected gains are first n_traits elements of E_vec
329339
E_M <- metrics$E_vec[1:n_traits]
330340
names(E_M) <- trait_names
@@ -495,17 +505,20 @@ gesim <- function(pmat, gmat, Gamma, selection_intensity = 2.063) {
495505
lambda2 <- ev_result$value
496506
b_G <- ev_result$vector
497507

498-
# Split into phenotype and GEBV coefficients
499-
b_y <- b_G[1:n_traits]
500-
b_gamma <- b_G[(n_traits + 1):(2 * n_traits)]
501-
502508
# --------------------------------------------------------------------------
503509
# Step 3: Compute metrics
504510
# --------------------------------------------------------------------------
505511
metrics <- .genomic_eigen_index_metrics(b_G, Phi, A,
506512
lambda2 = lambda2,
507513
k_I = selection_intensity)
508514

515+
# Extract corrected eigenvector (sign-corrected for positive quadratic form)
516+
b_G <- metrics$b
517+
518+
# Split into phenotype and GEBV coefficients
519+
b_y <- b_G[1:n_traits]
520+
b_gamma <- b_G[(n_traits + 1):(2 * n_traits)]
521+
509522
# Expected gains are first n_traits elements
510523
E_G <- metrics$E_vec[1:n_traits]
511524
names(E_G) <- trait_names
@@ -609,8 +622,8 @@ gesim <- function(pmat, gmat, Gamma, selection_intensity = 2.063) {
609622
#' \deqn{(\mathbf{Q}^{-1}\mathbf{X} - \lambda_W^2 \mathbf{I}_{(t+N)})\boldsymbol{\beta}_W = 0}
610623
#'
611624
#' where:
612-
#' \deqn{\mathbf{Q} = \begin{bmatrix} \mathbf{P} & \mathbf{G}_M^{\prime} \\ \mathbf{G}_M & \mathbf{M} \end{bmatrix}}
613-
#' \deqn{\mathbf{X} = \begin{bmatrix} \mathbf{C} & \mathbf{G}_M^{\prime} \\ \mathbf{G}_M & \mathbf{M} \end{bmatrix}}
625+
#' \deqn{\mathbf{Q} = \begin{bmatrix} \mathbf{P} & \mathbf{G}_M \\ \mathbf{G}_M^{\prime} & \mathbf{M} \end{bmatrix}}
626+
#' \deqn{\mathbf{X} = \begin{bmatrix} \mathbf{C} & \mathbf{G}_M \\ \mathbf{G}_M^{\prime} & \mathbf{M} \end{bmatrix}}
614627
#'
615628
#' \strong{Selection response:}
616629
#' \deqn{R_W = k_I \sqrt{\boldsymbol{\beta}_W^{\prime}\mathbf{Q}\boldsymbol{\beta}_W}}
@@ -673,18 +686,18 @@ gw_esim <- function(pmat, gmat, G_M, M, selection_intensity = 2.063) {
673686
# --------------------------------------------------------------------------
674687
# Step 1: Construct combined matrices Q and X
675688
# --------------------------------------------------------------------------
676-
# Q = [P G_M']
677-
# [G_M M ]
689+
# Q = [P G_M ]
690+
# [G_M' M ]
678691
Q <- rbind(
679-
cbind(pmat, t(G_M)),
680-
cbind(G_M, M)
692+
cbind(pmat, G_M),
693+
cbind(t(G_M), M)
681694
)
682695

683-
# X = [C G_M']
684-
# [G_M M ]
696+
# X = [C G_M ]
697+
# [G_M' M ]
685698
X <- rbind(
686-
cbind(gmat, t(G_M)),
687-
cbind(G_M, M)
699+
cbind(gmat, G_M),
700+
cbind(t(G_M), M)
688701
)
689702

690703
# --------------------------------------------------------------------------
@@ -696,17 +709,20 @@ gw_esim <- function(pmat, gmat, G_M, M, selection_intensity = 2.063) {
696709
lambda2 <- ev_result$value
697710
b_W <- ev_result$vector
698711

699-
# Split into phenotype and marker coefficients
700-
b_y <- b_W[1:n_traits]
701-
b_m <- b_W[(n_traits + 1):(n_traits + n_markers)]
702-
703712
# --------------------------------------------------------------------------
704713
# Step 3: Compute metrics
705714
# --------------------------------------------------------------------------
706715
metrics <- .genomic_eigen_index_metrics(b_W, Q, X,
707716
lambda2 = lambda2,
708717
k_I = selection_intensity)
709718

719+
# Extract corrected eigenvector (sign-corrected for positive quadratic form)
720+
b_W <- metrics$b
721+
722+
# Split into phenotype and marker coefficients
723+
b_y <- b_W[1:n_traits]
724+
b_m <- b_W[(n_traits + 1):(n_traits + n_markers)]
725+
710726
# Expected gains are first n_traits elements
711727
E_W <- metrics$E_vec[1:n_traits]
712728
names(E_W) <- trait_names
@@ -878,12 +894,9 @@ rgesim <- function(pmat, gmat, Gamma, U_mat, selection_intensity = 2.063) {
878894
# 2. The GEBV coefficient b_gamma[i] (row t+i)
879895
# Otherwise, the index bypasses the restriction by shifting weight to GEBV.
880896
#
881-
# If U_mat restricts trait i (has 1 in row i), then U_G must have:
882-
# - 1 in row i (phenotype)
883-
# - 1 in row t+i (GEBV)
884-
#
885-
# Solution: Stack U_mat twice to create (2t x r) restriction matrix
886-
U_G <- rbind(U_mat, U_mat) # [U_mat; U_mat] ensures both y and gamma are restricted
897+
# Solution: Concatenate U_mat horizontally to create (r x 2t) restriction matrix
898+
# This applies each constraint to both phenotype and GEBV coefficients
899+
U_G <- cbind(U_mat, U_mat) # [U_mat, U_mat] ensures both y and gamma are restricted
887900

888901
# --------------------------------------------------------------------------
889902
# Step 2: Compute constraint projection matrix Q_RG
@@ -913,17 +926,20 @@ rgesim <- function(pmat, gmat, Gamma, U_mat, selection_intensity = 2.063) {
913926
lambda2 <- ev_result$value
914927
b_RG <- ev_result$vector
915928

916-
# Split coefficients
917-
b_y <- b_RG[1:n_traits]
918-
b_gamma <- b_RG[(n_traits + 1):(2 * n_traits)]
919-
920929
# --------------------------------------------------------------------------
921930
# Step 4: Compute metrics
922931
# --------------------------------------------------------------------------
923932
metrics <- .genomic_eigen_index_metrics(b_RG, Phi, A,
924933
lambda2 = lambda2,
925934
k_I = selection_intensity)
926935

936+
# Extract corrected eigenvector (sign-corrected for positive quadratic form)
937+
b_RG <- metrics$b
938+
939+
# Split coefficients
940+
b_y <- b_RG[1:n_traits]
941+
b_gamma <- b_RG[(n_traits + 1):(2 * n_traits)]
942+
927943
E_RG <- metrics$E_vec[1:n_traits]
928944
names(E_RG) <- trait_names
929945

@@ -1173,17 +1189,20 @@ ppg_gesim <- function(pmat, gmat, Gamma, d, selection_intensity = 2.063) {
11731189
lambda2 <- ev_result$value
11741190
b_PG <- ev_result$vector
11751191

1176-
# Split coefficients
1177-
b_y <- b_PG[1:n_traits]
1178-
b_gamma <- b_PG[(n_traits + 1):(2 * n_traits)]
1179-
11801192
# --------------------------------------------------------------------------
11811193
# Step 6: Compute metrics
11821194
# --------------------------------------------------------------------------
11831195
metrics <- .genomic_eigen_index_metrics(b_PG, Phi, A,
11841196
lambda2 = lambda2,
11851197
k_I = selection_intensity)
11861198

1199+
# Extract corrected eigenvector (sign-corrected for positive quadratic form)
1200+
b_PG <- metrics$b
1201+
1202+
# Split coefficients
1203+
b_y <- b_PG[1:n_traits]
1204+
b_gamma <- b_PG[(n_traits + 1):(2 * n_traits)]
1205+
11871206
E_PG <- metrics$E_vec[1:n_traits]
11881207
names(E_PG) <- trait_names
11891208

R/helpers-validation.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,5 +274,8 @@ is_symmetric <- function(mat, tolerance = TOL_SYM) {
274274
#' @keywords internal
275275
#' @noRd
276276
is_zero <- function(x, tolerance = TOL_ZERO) {
277-
abs(x) < tolerance
277+
# Handle NA and Inf values
278+
result <- abs(x) < tolerance
279+
result[is.na(x) | is.infinite(x)] <- FALSE
280+
result
278281
}

man/gw_esim.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/RcppExports.gcda

-14.2 KB
Binary file not shown.

src/RcppExports.gcno

-546 KB
Binary file not shown.

src/RcppExports.o

-800 KB
Binary file not shown.

src/math_primitives.cpp

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,43 @@ Eigen::MatrixXd cpp_grouped_sums(
4141
const int n_obs = data_mat.rows();
4242
const int n_traits = data_mat.cols();
4343

44+
// Validate inputs
45+
if (n_obs == 0 || n_traits == 0) {
46+
return MatrixXd::Zero(0, n_traits);
47+
}
48+
if (group_idx.size() != n_obs) {
49+
Rcpp::stop("group_idx size must match data_mat rows");
50+
}
51+
4452
// Convert 1-based R indices to 0-based C++
4553
Eigen::VectorXi groups = group_idx.array() - 1;
4654

47-
// Count unique groups
48-
int n_groups = groups.maxCoeff() + 1;
55+
// Validate group indices are non-negative
56+
if (groups.minCoeff() < 0) {
57+
Rcpp::stop("group_idx must contain positive integers");
58+
}
4959

50-
// Pre-allocate result matrix
60+
// Find actual number of unique groups safely
61+
int min_group = groups.minCoeff();
62+
int max_group = groups.maxCoeff();
63+
int n_groups = max_group + 1;
64+
65+
// Validate dimensions before matrix allocation
66+
if (n_groups <= 0 || n_traits <= 0) {
67+
Rcpp::stop("Invalid matrix dimensions: n_groups=" + std::to_string(n_groups) +
68+
", n_traits=" + std::to_string(n_traits));
69+
}
70+
71+
// Pre-allocate result matrix with validated dimensions
5172
MatrixXd result(n_groups, n_traits);
5273
result.setZero();
5374

5475
// Accumulate sums for each group
5576
for (int i = 0; i < n_obs; ++i) {
56-
result.row(groups(i)) += data_mat.row(i);
77+
int group = groups(i);
78+
if (group >= 0 && group < n_groups) {
79+
result.row(group) += data_mat.row(i);
80+
}
5781
}
5882

5983
return result;
@@ -81,17 +105,43 @@ List cpp_multi_grouped_sums(
81105
const int n_traits = data_mat.cols();
82106
const int n_groupings = group_indices.size();
83107

108+
// Validate inputs
109+
if (n_obs == 0 || n_traits == 0) {
110+
List result(n_groupings);
111+
for (int g = 0; g < n_groupings; ++g) {
112+
result[g] = MatrixXd::Zero(0, n_traits);
113+
}
114+
return result;
115+
}
116+
84117
List result(n_groupings);
85118

86119
for (int g = 0; g < n_groupings; ++g) {
87120
Eigen::VectorXi groups = Rcpp::as<Eigen::Map<Eigen::VectorXi>>(group_indices[g]).array() - 1;
121+
122+
// Validate group indices
123+
if (groups.size() != n_obs) {
124+
Rcpp::stop("group_indices[" + std::to_string(g) + "] size must match data_mat rows");
125+
}
126+
if (groups.minCoeff() < 0) {
127+
Rcpp::stop("group_indices[" + std::to_string(g) + "] must contain positive integers");
128+
}
129+
88130
int n_groups = groups.maxCoeff() + 1;
89131

132+
// Validate dimensions
133+
if (n_groups <= 0) {
134+
Rcpp::stop("Invalid number of groups: " + std::to_string(n_groups));
135+
}
136+
90137
MatrixXd group_sums(n_groups, n_traits);
91138
group_sums.setZero();
92139

93140
for (int i = 0; i < n_obs; ++i) {
94-
group_sums.row(groups(i)) += data_mat.row(i);
141+
int group = groups(i);
142+
if (group >= 0 && group < n_groups) {
143+
group_sums.row(group) += data_mat.row(i);
144+
}
95145
}
96146

97147
result[g] = group_sums;

src/math_primitives.gcda

-91.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)