Skip to content

Commit 98769f2

Browse files
rowless parents add
rowless parent fix
1 parent b45aa40 commit 98769f2

10 files changed

Lines changed: 443 additions & 70 deletions

‎NEWS.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
## Development version
44

5-
* Add building of parental chains with `addParentalChain()` and `addParentalFlag()`, so that you can easily trace maternity and paternity. This is implemented as a general function that can be used to build any type of parental chain, and a specific wrapper for maternal and paternal chains. The parental flag is a binary variable that indicates whether the individual is in the specified parental chain, which can be useful for filtering or grouping individuals based on their lineage.
5+
* Add building of parental chains with `addParentalChain()` and `addParentalFlag()`, so that you can easily trace maternity and paternity. This is implemented as a general function that can be used to build any parental chain, and a specific wrapper for maternal and paternal chains. The parental flag is a binary variable that indicates whether the individual is in the specified parental chain, which can be useful for filtering or grouping individuals based on their lineage.
6+
* Fixed silent mis-scoring in `ped2com()`/`ped2add()` (and other component wrappers) when `momID`/`dadID` reference a parent ID that has no row of its own in `ped` (e.g., unrecorded founder stock, or a pedigree subset that excludes the parent's own row). Previously, `isChild_method = "partialparent"` treated such a parent as "known" (based on `momID`/`dadID` not being `NA`) while the adjacency builders correctly treated the link as absent, so the diagonal was silently understated (e.g., 0.5/0.75 instead of 1) and covariance between siblings who shared the missing parent was lost entirely. `ped2com()` now warns when this is detected. Two solutions are available in the new `repair_rowless_parents = TRUE` argument.
7+
* \code{"rows"} (default) adds one placeholder founder row per unique missing parent ID (not one per affected child) to a working copy of \code{ped}, then restricts the returned matrix back to the original individuals via \code{keep_ids} unless \code{keep_ids} is already supplied.
8+
* \code{"schur"} applies a Schur complement on the block-triangular RAM system -- for each missing parent, its already-known children define a rank-1 update (\code{v \%*\% t(v)}, where \code{v} is that parent's traced genetic contribution to every individual, computed from the existing RAM matrix) which is added to the relatedness matrix at the tcrossprod step. \code{"schur"} currently only supports \code{component = "additive"}.
69

710
## BGmisc 1.8.0
811
* Optimized gedcom reader, com2links for speed and memory usage, with a focus on large pedigrees

‎R/addParentalChain.R‎

Lines changed: 48 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@
2727
#'
2828
#' @export
2929
addPaternalChain <- function(
30-
ped,
31-
personID = "personID",
32-
dadID = "dadID",
33-
momID = "momID",
34-
chain_col = "dadID_chain",
35-
chain_string_col = "dadID_chain_string",
36-
collapse = "|",
37-
traversal_direction = "in"
30+
ped,
31+
personID = "personID",
32+
dadID = "dadID",
33+
momID = "momID",
34+
chain_col = "dadID_chain",
35+
chain_string_col = "dadID_chain_string",
36+
collapse = "|",
37+
traversal_direction = "in"
3838
) {
39-
addParentalChain(
39+
addParentalChain(
4040
ped = ped,
4141
personID = personID,
4242
dadID = dadID,
@@ -46,7 +46,7 @@ addParentalChain(
4646
collapse = collapse,
4747
component = "dadID",
4848
traversal_direction = traversal_direction
49-
)
49+
)
5050
}
5151

5252
#' Add maternal ancestor chains to a pedigree
@@ -72,16 +72,16 @@ addParentalChain(
7272
#' @export
7373

7474
addMaternalChain <- function(
75-
ped,
76-
personID = "personID",
77-
dadID = "dadID",
78-
momID = "momID",
79-
chain_col = "momID_chain",
80-
chain_string_col = "momID_chain_string",
81-
collapse = "|",
82-
traversal_direction = "in"
75+
ped,
76+
personID = "personID",
77+
dadID = "dadID",
78+
momID = "momID",
79+
chain_col = "momID_chain",
80+
chain_string_col = "momID_chain_string",
81+
collapse = "|",
82+
traversal_direction = "in"
8383
) {
84-
addParentalChain(
84+
addParentalChain(
8585
ped = ped,
8686
personID = personID,
8787
dadID = dadID,
@@ -91,7 +91,7 @@ addParentalChain(
9191
collapse = collapse,
9292
component = "momID",
9393
traversal_direction = traversal_direction
94-
)
94+
)
9595
}
9696
#' Add unilineal parental ancestor chains to a pedigree
9797
#'
@@ -139,17 +139,16 @@ addParentalChain(
139139
#' @export
140140

141141
addParentalChain <- function(
142-
ped,
143-
personID = "personID",
144-
dadID = "dadID",
145-
momID = "momID",
146-
chain_col = "chain",
147-
chain_string_col = "chain_string",
148-
collapse = "|",
149-
component = c("dadID", "momID"),
150-
traversal_direction = "in"
142+
ped,
143+
personID = "personID",
144+
dadID = "dadID",
145+
momID = "momID",
146+
chain_col = "chain",
147+
chain_string_col = "chain_string",
148+
collapse = "|",
149+
component = c("dadID", "momID"),
150+
traversal_direction = "in"
151151
) {
152-
153152
# Build a paternal-only version of the pedigree.
154153
# This removes maternal edges so the resulting graph only represents:
155154
# person -> father -> father's father -> father's father's father.
@@ -165,21 +164,21 @@ addParentalChain <- function(
165164
personID = as.character(ped[[personID]]),
166165
momID = as.character(ped[[momID]]),
167166
dadID = NA_character_,
168-
stringsAsFactors = FALSE
167+
stringsAsFactors = FALSE
169168
)
170169
} else if (component == "dadID") {
171170
##############
172171
# Paternal-only pedigree.
173172
##############
174-
parental_ped <- data.frame(
173+
parental_ped <- data.frame(
175174
personID = as.character(ped[[personID]]),
176175
momID = NA_character_,
177176
dadID = as.character(ped[[dadID]]),
178-
stringsAsFactors = FALSE
177+
stringsAsFactors = FALSE
179178
)
180-
} else {
181-
stop("unknown id supplied")
182-
}
179+
} else {
180+
stop("unknown id supplied")
181+
}
183182
# Use BGmisc infrastructure to convert the paternal-only pedigree into a graph.
184183
parental_graph <- ped2graph(
185184
parental_ped,
@@ -190,7 +189,6 @@ addParentalChain <- function(
190189

191190
# For one person, recover the ordered paternal chain from the network.
192191
get_ordered_parental_chain <- function(id) {
193-
194192
id_chr <- as.character(id)
195193

196194
# If the person is not represented as a graph vertex, return no chain.
@@ -272,10 +270,10 @@ addParentalChain <- function(
272270
#'
273271
#' @export
274272
addPaternalLineFlag <- function(
275-
ped,
276-
anchor_id,
277-
flag_col,
278-
chain_col = "dadID_chain"
273+
ped,
274+
anchor_id,
275+
flag_col,
276+
chain_col = "dadID_chain"
279277
) {
280278
addParentalLineFlag(
281279
ped = ped,
@@ -301,10 +299,10 @@ addPaternalLineFlag <- function(
301299
#' @export
302300

303301
addMaternalLineFlag <- function(
304-
ped,
305-
anchor_id,
306-
flag_col,
307-
chain_col = "momID_chain"
302+
ped,
303+
anchor_id,
304+
flag_col,
305+
chain_col = "momID_chain"
308306
) {
309307
addParentalLineFlag(
310308
ped = ped,
@@ -343,13 +341,12 @@ addMaternalLineFlag <- function(
343341
#'
344342
#' @export
345343
addParentalLineFlag <- function(
346-
ped,
347-
anchor_id,
348-
flag_col,
349-
chain_col,
350-
component = c("dadID", "momID")
344+
ped,
345+
anchor_id,
346+
flag_col,
347+
chain_col,
348+
component = c("dadID", "momID")
351349
) {
352-
353350
component <- match.arg(component)
354351

355352
# Convert the anchor ID to character because the parental chain stores graph

‎R/buildComponent.R‎

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#' @param keep_ids character vector of IDs to retain in the final relatedness matrix. When supplied, only the rows of \code{r2} corresponding to these IDs are used in the tcrossprod, so the result is a \code{length(keep_ids) x length(keep_ids)} matrix. All columns of \code{r2} are retained during the multiplication so relatedness values remain correct. IDs not found in the pedigree are silently dropped with a warning.
2020
#' @param adjacency_method character. The method to use for computing the adjacency matrix. Options are "loop", "indexed", direct or beta
2121
#' @param isChild_method character. The method to use for computing the isChild matrix. Options are "classic" or "partialparent"
22+
#' @param repair_rowless_parents logical. If TRUE, automatically correct for parents referenced in momID/dadID that have no row of their own in \code{ped} (e.g., unrecorded founder stock), before computing relatedness. Without this, such parents are still treated as "known" for the purposes of \code{isChild_method = "partialparent"} even though their genetic contribution cannot be traced, which understates diagonal relatedness and drops covariance between siblings who share the missing parent. How the correction is applied is controlled by \code{rowless_parents_method}. Defaults to FALSE, in which case a warning is issued instead.
23+
#' @param rowless_parents_method character. How to apply the \code{repair_rowless_parents} correction. \code{"rows"} (default) adds one placeholder founder row per unique missing parent ID (not one per affected child) to a working copy of \code{ped}, then restricts the returned matrix back to the original individuals via \code{keep_ids} unless \code{keep_ids} is already supplied. \code{"schur"} applies a Schur complement on the block-triangular RAM system -- for each missing parent, its already-known children define a rank-1 update (\code{v \%*\% t(v)}, where \code{v} is that parent's traced genetic contribution to every individual, computed from the existing RAM matrix) which is added to the relatedness matrix at the tcrossprod step. \code{"schur"} currently only supports \code{component = "additive"}.
2224
#' @param adjBeta_method numeric The method to use for computing the building the adjacency_method matrix when using the "beta" build
2325
#' @param compress logical. If TRUE, use compression when saving the checkpoint files. Defaults to TRUE.
2426
#' @param mz_twins logical. If TRUE, merge MZ co-twin columns in the r2 matrix before tcrossprod so that MZ twins are coded with relatedness 1 instead of 0.5. Twin pairs are identified from the \code{twinID} column. When a \code{zygosity} column is also present, only pairs where both members have \code{zygosity == "MZ"} are used; otherwise all \code{twinID} pairs are assumed to be MZ. Defaults to TRUE.
@@ -41,6 +43,8 @@ ped2com <- function(ped, component,
4143
keep_ids = NULL,
4244
adjacency_method = "direct",
4345
isChild_method = "partialparent",
46+
repair_rowless_parents = FALSE,
47+
rowless_parents_method = "rows",
4448
saveable = FALSE,
4549
resume = FALSE,
4650
save_rate = 5,
@@ -71,6 +75,8 @@ ped2com <- function(ped, component,
7175
transpose_method = transpose_method,
7276
adjacency_method = adjacency_method,
7377
isChild_method = isChild_method,
78+
repair_rowless_parents = repair_rowless_parents,
79+
rowless_parents_method = rowless_parents_method,
7480
save_rate = save_rate,
7581
save_rate_gen = save_rate_gen,
7682
save_rate_parlist = save_rate_parlist,
@@ -133,11 +139,76 @@ ped2com <- function(ped, component,
133139
))
134140
}
135141

142+
# Validate the 'rowless_parents_method' argument
143+
rowless_parents_method_options <- c("rows", "schur")
144+
if (!config$rowless_parents_method %in% rowless_parents_method_options) {
145+
stop(paste0(
146+
"Invalid rowless_parents_method specified. Choose from ",
147+
paste(rowless_parents_method_options, collapse = ", "), "."
148+
))
149+
}
150+
# "schur" is a pure argument-compatibility constraint, checked here for the
151+
# same reason adjacency_method/transpose_method are validated up front --
152+
# this is NOT the correction itself running early. Components that return
153+
# before the tcrossprod step (generation/distance/common nuclear) would
154+
# otherwise never reach the check below and silently skip the correction.
155+
if (isTRUE(config$repair_rowless_parents) && config$rowless_parents_method == "schur" &&
156+
config$component != "additive") {
157+
stop(
158+
"repair_rowless_parents = TRUE with rowless_parents_method = \"schur\" is currently ",
159+
"only implemented for component = \"additive\". Use rowless_parents_method = \"rows\" ",
160+
"for other components."
161+
)
162+
}
163+
136164
# standardize colnames
137165
if (config$standardize_colnames == TRUE) {
138166
ped <- standardizeColnames(ped, verbose = config$verbose)
139167
}
140168

169+
# Detect parents referenced in momID/dadID that have no row of their own
170+
# (e.g., unrecorded founder stock). Left unrepaired, isChild_method =
171+
# "partialparent" (based on raw NA-status of momID/dadID) and the adjacency
172+
# builders (based on actual row matches) disagree about whether such a
173+
# parent is "known," which understates diagonal relatedness and silently
174+
# drops covariance between siblings who share the same missing parent.
175+
# rowless_parents_method = "rows" is handled entirely here, since it must
176+
# mutate `ped` before anything downstream (isPar/isChild/RAM tracing) runs.
177+
# rowless_parents_method = "schur" touches nothing here -- it needs the RAM
178+
# matrix `r`, so its validation and correction both live at the tcrossprod
179+
# step below, right where the relatedness matrix is actually assembled.
180+
rowless_parents <- .findRowlessParents(ped)
181+
if (length(rowless_parents) > 0) {
182+
if (isTRUE(config$repair_rowless_parents) && config$rowless_parents_method == "rows") {
183+
if (config$verbose == TRUE) {
184+
message(
185+
length(rowless_parents), " parent ID(s) referenced in momID/dadID have no row in `ped`. ",
186+
"Adding one placeholder founder row per missing parent before computing relatedness.\n"
187+
)
188+
}
189+
original_ids <- ped$ID
190+
ped <- addRowlessParents(
191+
ped = ped, verbose = config$verbose,
192+
validation_results = list(female_var = NA, male_var = NA)
193+
)
194+
config$nr <- nrow(ped)
195+
if (is.null(config$keep_ids)) {
196+
keep_ids <- original_ids
197+
config$keep_ids <- original_ids
198+
}
199+
} else if (!isTRUE(config$repair_rowless_parents)) {
200+
warning(
201+
length(rowless_parents), " parent ID(s) referenced in momID/dadID have no matching row in `ped`. ",
202+
"isChild_method = \"", config$isChild_method, "\" and the adjacency builders will disagree about ",
203+
"whether these parents are \"known,\" which can understate diagonal relatedness (e.g., 0.5/0.75 ",
204+
"instead of 1) and omit covariance between siblings who share the missing parent. Set ",
205+
"repair_rowless_parents = TRUE (with rowless_parents_method = \"rows\" or \"schur\") to correct ",
206+
"for them, or repair `ped` yourself first with checkParentIDs(ped, repair = TRUE, parentswithoutrow = TRUE).",
207+
call. = FALSE
208+
)
209+
}
210+
}
211+
141212
mz_row_pairs <- NULL
142213
mz_id_pairs <- NULL
143214

@@ -409,6 +480,44 @@ ped2com <- function(ped, component,
409480
}
410481
}
411482
}
483+
484+
# --- Step 3c: Schur-complement correction for rowless parents ---
485+
# For each parent referenced in momID/dadID with no row of its own, its
486+
# (known) children define a rank-1 update: v = r %*% p_f, where p_f places
487+
# `parVal` at each of that parent's children and r is the not-yet-isChild-
488+
# scaled RAM matrix (I-A)^-1. Appending v as an extra column of r2 makes
489+
# the upcoming tcrossprod add v %*% t(v) for free -- this is exactly the
490+
# contribution that parent would have made had it been a real row, derived
491+
# via a Schur complement on the block-triangular (rowless-parents-first)
492+
# system. No row is ever added to `ped`, and the matrix's row dimension
493+
# (and therefore dimnames) never changes.
494+
if (length(rowless_parents) > 0 && isTRUE(config$repair_rowless_parents) &&
495+
config$rowless_parents_method == "schur") {
496+
# component compatibility already validated up front
497+
if (config$verbose == TRUE) {
498+
message(
499+
length(rowless_parents), " parent ID(s) referenced in momID/dadID have no row in `ped`. ",
500+
"Applying an exact low-rank (Schur complement) correction for them here at the tcrossprod ",
501+
"step; `ped` and matrix dimensions are left untouched.\n"
502+
)
503+
}
504+
p_nf <- matrix(0,
505+
nrow = config$nr, ncol = length(rowless_parents),
506+
dimnames = list(NULL, rowless_parents)
507+
)
508+
for (j in seq_along(rowless_parents)) {
509+
f <- rowless_parents[j]
510+
p_nf[, j] <- parVal * (
511+
(!is.na(ped$momID) & ped$momID == f) + (!is.na(ped$dadID) & ped$dadID == f)
512+
)
513+
}
514+
v <- as.matrix(r %*% p_nf)
515+
r2 <- cbind(r2, v)
516+
if (config$verbose == TRUE) {
517+
message("Added ", length(rowless_parents), " rank-1 rowless-parent correction column(s) to r2")
518+
}
519+
}
520+
412521
# --- Step 4: T crossproduct ---
413522

414523
# Subset rows of r2 to target individuals if requested.
@@ -672,7 +781,7 @@ ped2com <- function(ped, component,
672781
isPar <- loadOrComputeCheckpoint(
673782
file = checkpoint_files$isPar,
674783
compute_fn = function() {
675-
if(is.null(iss) || is.null(jss) || length(iss) == 0 || length(jss) == 0) {
784+
if (is.null(iss) || is.null(jss) || length(iss) == 0 || length(jss) == 0) {
676785
warning("Cannot construct isPar matrix: iss or jss is NULL or empty.")
677786
}
678787

‎R/buildmxPedigrees.R‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,13 @@ fitPedigreeModel <- function(
404404
ci = intervals,
405405
condenseMatrixSlots = FALSE # only need to condense once
406406
)
407-
if(runmodel == TRUE){
408-
if (tryhard == TRUE) {
409-
fitted_model <- OpenMx::mxTryHard(pedigree_model, silent = TRUE, extraTries = extraTries, intervals = intervals)
410-
} else {
411-
fitted_model <- OpenMx::mxRun(pedigree_model, intervals = intervals)
412-
}
407+
if (runmodel == TRUE) {
408+
if (tryhard == TRUE) {
409+
fitted_model <- OpenMx::mxTryHard(pedigree_model, silent = TRUE, extraTries = extraTries, intervals = intervals)
413410
} else {
411+
fitted_model <- OpenMx::mxRun(pedigree_model, intervals = intervals)
412+
}
413+
} else {
414414
fitted_model <- pedigree_model
415415
}
416416

0 commit comments

Comments
 (0)