From cd3b6ad2ced3a39c2f1049ad868af1ba228c2db3 Mon Sep 17 00:00:00 2001 From: Mason Garrison <6001608+smasongarrison@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:10:07 -0400 Subject: [PATCH 01/11] Update helpGeneric.R --- R/helpGeneric.R | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/R/helpGeneric.R b/R/helpGeneric.R index d6711fb7..f993387f 100644 --- a/R/helpGeneric.R +++ b/R/helpGeneric.R @@ -104,6 +104,38 @@ resample <- function(x, ...) { x[sample.int(length(x), ...)] } +#' Coerce to a Numeric Matrix +#' +#' @param x An object coercible to a matrix. +#' @keywords internal +#' @return \code{x} as a matrix with numeric storage mode. +#' +as_numeric_matrix <- function(x) { + x <- as.matrix(x) + storage.mode(x) <- "numeric" + x +} + +#' Symmetrize a Matrix +#' +#' Averages a matrix with its transpose when it is not already symmetric +#' within tolerance. Used to clean up relatedness matrices before they are +#' passed to OpenMx, which requires exact symmetry for \code{"Symm"}-type +#' \code{mxMatrix} objects. +#' +#' @param x A square matrix, or an object coercible to one. +#' @param tol Numeric tolerance for asymmetry. Default is 1e-10. +#' @keywords internal +#' @return A numeric matrix, symmetrized if needed. +#' +make_symmetric <- function(x, tol = 1e-10) { + x <- as_numeric_matrix(x) + if (max(abs(x - t(x)), na.rm = TRUE) > tol) { + x <- (x + t(x)) / 2 + } + x +} + #' Check for OpenMx Package #' #' This function checks if the OpenMx package is installed and available for use. If the package is not installed, it throws an error with a message indicating that OpenMx is required and provides instructions for installation. From d6d2c1a85a326521f5675f0e7f0fef918e971b96 Mon Sep 17 00:00:00 2001 From: Mason Garrison <6001608+smasongarrison@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:02:13 -0400 Subject: [PATCH 02/11] fix check id error --- R/checkParents.R | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/R/checkParents.R b/R/checkParents.R index 44b7b46f..6b32dd07 100644 --- a/R/checkParents.R +++ b/R/checkParents.R @@ -110,16 +110,18 @@ checkParentIDs <- function(ped, verbose = FALSE, repair = FALSE, } # Are any parents in both momID and dadID? momdad <- intersect(ped$dadID, ped$momID) - if (length(momdad) > 0 && !is.na(momdad)) { - validation_results$parents_in_both <- momdad - if (verbose == TRUE) { - cat(paste( - "Some individuals appear in both momID and dadID roles.\n", - "These individuals are:\n" - )) - message(momdad) - } + momdad <- momdad[!is.na(momdad)] +if (length(momdad) > 0) { + validation_results$parents_in_both <- momdad + if (verbose == TRUE) { + cat( + "Some individuals appear in both momID and dadID roles.\n", + "These individuals are:\n", + sep = "" + ) + message(paste(momdad, collapse = ", ")) } +} if (!repair) { From a0c81c25f2a35fd9f6c56d476f16aea38bd7f0a5 Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sat, 11 Jul 2026 11:33:37 -0400 Subject: [PATCH 03/11] docs --- man/as_numeric_matrix.Rd | 18 ++++++++++++++++++ man/make_symmetric.Rd | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 man/as_numeric_matrix.Rd create mode 100644 man/make_symmetric.Rd diff --git a/man/as_numeric_matrix.Rd b/man/as_numeric_matrix.Rd new file mode 100644 index 00000000..5568862a --- /dev/null +++ b/man/as_numeric_matrix.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helpGeneric.R +\name{as_numeric_matrix} +\alias{as_numeric_matrix} +\title{Coerce to a Numeric Matrix} +\usage{ +as_numeric_matrix(x) +} +\arguments{ +\item{x}{An object coercible to a matrix.} +} +\value{ +\code{x} as a matrix with numeric storage mode. +} +\description{ +Coerce to a Numeric Matrix +} +\keyword{internal} diff --git a/man/make_symmetric.Rd b/man/make_symmetric.Rd new file mode 100644 index 00000000..04f8014d --- /dev/null +++ b/man/make_symmetric.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helpGeneric.R +\name{make_symmetric} +\alias{make_symmetric} +\title{Symmetrize a Matrix} +\usage{ +make_symmetric(x, tol = 1e-10) +} +\arguments{ +\item{x}{A square matrix, or an object coercible to one.} + +\item{tol}{Numeric tolerance for asymmetry. Default is 1e-10.} +} +\value{ +A numeric matrix, symmetrized if needed. +} +\description{ +Averages a matrix with its transpose when it is not already symmetric +within tolerance. Used to clean up relatedness matrices before they are +passed to OpenMx, which requires exact symmetry for \code{"Symm"}-type +\code{mxMatrix} objects. +} +\keyword{internal} From 466b49ba8a9180fcd893eb2c75d903286264e8c9 Mon Sep 17 00:00:00 2001 From: Mason Garrison <6001608+smasongarrison@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:37:30 -0400 Subject: [PATCH 04/11] Update helpGeneric.R --- R/helpGeneric.R | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/R/helpGeneric.R b/R/helpGeneric.R index f993387f..0c350777 100644 --- a/R/helpGeneric.R +++ b/R/helpGeneric.R @@ -129,6 +129,13 @@ as_numeric_matrix <- function(x) { #' @return A numeric matrix, symmetrized if needed. #' make_symmetric <- function(x, tol = 1e-10) { + if (inherits(x, "sparseMatrix")) { + if (!Matrix::isSymmetric(x, tol = tol)) { + x <- (x + Matrix::t(x)) / 2 + } + + return(Matrix::forceSymmetric(x, uplo = "L")) + } x <- as_numeric_matrix(x) if (max(abs(x - t(x)), na.rm = TRUE) > tol) { x <- (x + t(x)) / 2 From b7052b48e7a13f85fc71dc4540de8fa01976808f Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 10:41:10 -0400 Subject: [PATCH 05/11] adding in select innovations from temporal branch --- NAMESPACE | 1 + R/buildmxPedigrees.R | 528 +++++++++++++++++++++---- man/buildFamilyGroups.Rd | 4 +- man/buildFamilyGroups_list.Rd | 57 +++ man/buildOneFamilyGroup.Rd | 4 +- man/buildPedigreeMx.Rd | 6 +- man/dot-assemblePedigreeMx.Rd | 28 ++ man/dot-buildGroupedStaticFamily.Rd | 60 +++ man/dot-pedigreeFamilySize.Rd | 19 + man/dot-pedigreeMeanMatrix.Rd | 23 ++ man/dot-pedigreeRelatednessMatrix.Rd | 33 ++ man/make_clean_personids.Rd | 17 + tests/testthat/test-buildmxPedigrees.R | 4 +- 13 files changed, 713 insertions(+), 71 deletions(-) create mode 100644 man/buildFamilyGroups_list.Rd create mode 100644 man/dot-assemblePedigreeMx.Rd create mode 100644 man/dot-buildGroupedStaticFamily.Rd create mode 100644 man/dot-pedigreeFamilySize.Rd create mode 100644 man/dot-pedigreeMeanMatrix.Rd create mode 100644 man/dot-pedigreeRelatednessMatrix.Rd create mode 100644 man/make_clean_personids.Rd diff --git a/NAMESPACE b/NAMESPACE index de1c947c..12786ebd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ export(addPaternalLineFlag) export(addPersonToPed) export(alignPhenToMatrix) export(buildFamilyGroups) +export(buildFamilyGroups_list) export(buildOneFamilyGroup) export(buildPedigreeModelCovariance) export(buildPedigreeMx) diff --git a/R/buildmxPedigrees.R b/R/buildmxPedigrees.R index 56eeb2c1..6ec04664 100644 --- a/R/buildmxPedigrees.R +++ b/R/buildmxPedigrees.R @@ -93,6 +93,66 @@ buildPedigreeModelCovariance <- function( do.call(OpenMx::mxModel, c(list("ModelOne"), mat_list)) } +#' Determine family size from a family's relatedness matrices (internal) +#' +#' @param mats_in A list of relatedness matrices (possibly containing NULLs); the first +#' non-NULL matrix's row count is used as the family size. +#' @return Integer family size. +#' @keywords internal +.pedigreeFamilySize <- function(mats_in) { + for (m in mats_in) { + if (!is.null(m)) { + return(nrow(m)) + } + } + stop("At least one relatedness matrix must be provided.") +} + +#' Build a relatedness mxMatrix (internal) +#' +#' Builds the \code{"Symm"} mxMatrix for one relatedness matrix. +#' +#' @param mat The relatedness matrix. +#' @param fsize Family size. +#' @param name The mxMatrix's name (e.g. "A", "Cn"). +#' @param condense Logical. If TRUE, apply \code{\link{condenseMatrixSlots}}. +#' @param symmetrize Logical. If TRUE, symmetrize \code{mat} via \code{\link{make_symmetric}} +#' before use; if FALSE, coerce via \code{as.matrix} only. +#' @return An mxMatrix object. +#' @keywords internal +.pedigreeRelatednessMatrix <- function(mat, fsize, name, condense = TRUE, symmetrize = FALSE) { + if (is.null(mat)) stop("Relatedness matrix cannot be NULL.") + # keep dense if dense, or sparse if sparse, but symmetrize if requested + if (inherits(mat, "Matrix")) { + values <- if (symmetrize) make_symmetric(mat) else mat + } else { + values <- if (symmetrize) make_symmetric(mat) else as.matrix(mat) + } + m <- OpenMx::mxMatrix( + type = "Symm", nrow = fsize, ncol = fsize, free = FALSE, + values = values, name = name + ) + if (condense) m <- condenseMatrixSlots(m) + m +} + +#' Build the free mean mxMatrix for a family group (internal) +#' +#' Shared by the static and temporal branches of \code{\link{buildOneFamilyGroup}}; they +#' only differ in the parameter label ("meanLI" vs "mean_y"). +#' +#' @param fsize Family size. +#' @param obs_ids Character vector of individual IDs. +#' @param label The mean parameter's label. +#' @return An mxMatrix object. +#' @keywords internal +.pedigreeMeanMatrix <- function(fsize, obs_ids, label) { + OpenMx::mxMatrix( + "Full", + nrow = 1, ncol = fsize, name = "M", free = TRUE, + values = 0, labels = label, dimnames = list(NULL, obs_ids) + ) +} #' Build one family group model #' @@ -130,19 +190,26 @@ buildOneFamilyGroup <- function( Dmgmat = NULL, full_df_row, obs_ids, - condenseMatrixSlots = TRUE + symmetrize = FALSE, + condenseMatrixSlots = TRUE, + clean_ids = FALSE ) { .require_openmx("buildOneFamilyGroup") - - # Determine family size from first available matrix - fsize <- NULL - for (m in list(Addmat, Nucmat, Extmat, Mtdmat, Amimat, Dmgmat)) { - if (!is.null(m)) { - fsize <- nrow(m) - break - } + if (clean_ids) { + obs_ids <- make_clean_personids(obs_ids) + # clean once + clean_ids <- FALSE + } + # Determine family size from first available matrix. Shared by both branches below. + if ( # not any of the matrices are provided + is.null(Addmat) && is.null(Dmgmat) && is.null(Nucmat) && + is.null(Extmat) && is.null(Mtdmat) && is.null(Amimat) + ) { + warning("At least one relatedness matrix should be provided. Using the number of columns in 'full_df_row' as family size.") + fsize <- ncol(full_df_row) + } else { + fsize <- .pedigreeFamilySize(list(Addmat, Dmgmat, Nucmat, Extmat, Mtdmat, Amimat)) } - if (is.null(fsize)) stop("At least one relatedness matrix must be provided.") # If Extmat is requested but not supplied as a matrix, create a unit matrix # (all members share the extended environment equally). @@ -150,35 +217,61 @@ buildOneFamilyGroup <- function( Extmat <- matrix(1, nrow = fsize, ncol = fsize) } - # ------------------------------------------------------------------ - # Build the list of mxMatrix objects and the algebra terms - # Each entry: list(mat = input_matrix, mxname, algebra_term). - # ------------------------------------------------------------------ + # One canonical table describing each variance component: which relatedness matrix it + # uses, its mxMatrix name, the exact static Kronecker algebra term (term, unchanged from + # the original static implementation), and the temporal loading/loading-matrix names + # (k / K). Both the static and temporal algebra below are built from this same table and + # the same relatedness mxMatrix objects (relmat_list), rather than each branch rebuilding + # its own copy of this table. mat_spec <- list( - list(mat = Addmat, mxname = "A", term = "(A %x% ModelOne.Vad)"), - list(mat = Dmgmat, mxname = "D", term = "(D %x% ModelOne.Vdd)"), - list(mat = Nucmat, mxname = "Cn", term = "(Cn %x% ModelOne.Vcn)"), - list(mat = Extmat, mxname = "Ce", term = "(Ce %x% ModelOne.Vce)"), - list(mat = Amimat, mxname = "Am", term = "(Am %x% ModelOne.Vam)"), - list(mat = Mtdmat, mxname = "Mt", term = "(Mt %x% ModelOne.Vmt)") + list( + mat = Addmat, + mxname = "A", + term = "(A %x% ModelOne.Vad)", + k = "a", K = "Ka" + ), + list( + mat = Dmgmat, + mxname = "D", + term = "(D %x% ModelOne.Vdd)", + k = "d", K = "Kd" + ), + list( + mat = Nucmat, + mxname = "Cn", + term = "(Cn %x% ModelOne.Vcn)", + k = "cn", K = "Kcn" + ), + list( + mat = Extmat, + mxname = "Ce", + term = "(Ce %x% ModelOne.Vce)", + k = "ce", K = "Kce" + ), + list( + mat = Amimat, + mxname = "Am", + term = "(Am %x% ModelOne.Vam)", + k = "am", K = "Kam" + ), + list( + mat = Mtdmat, + mxname = "Mt", + term = "(Mt %x% ModelOne.Vmt)", + k = "mt", K = "Kmt" + ) ) active <- Filter(function(s) !is.null(s$mat), mat_spec) - if (condenseMatrixSlots) { - relmat_list <- lapply(active, function(s) { - condenseMatrixSlots(OpenMx::mxMatrix("Symm", - nrow = fsize, ncol = fsize, - values = as.matrix(s$mat), name = s$mxname - )) - }) - } else { - relmat_list <- lapply(active, function(s) { - OpenMx::mxMatrix("Symm", - nrow = fsize, ncol = fsize, - values = as.matrix(s$mat), name = s$mxname - ) - }) - } + # Static values are used as-is (as.matrix); temporal symmetrizes them first. Either + # way, this single loop replaces what used to be two separate (and, for static, + # itself duplicated condensed/uncondensed) copies of the same mxMatrix-building code. + relmat_list <- lapply(active, function(s) { + .pedigreeRelatednessMatrix( + s$mat, fsize, s$mxname, + condense = condenseMatrixSlots, symmetrize = symmetrize + ) + }) # add the identity matrix for the unique environment, which is always included as a term in the algebra mat_list <- c( list(OpenMx::mxMatrix("Iden", nrow = fsize, ncol = fsize, name = "I")), @@ -198,12 +291,9 @@ buildOneFamilyGroup <- function( mat_list, list( OpenMx::mxData(observed = full_df_row, type = "raw", sort = FALSE), - OpenMx::mxMatrix("Full", - nrow = 1, ncol = fsize, name = "M", free = TRUE, - labels = "meanLI", dimnames = list(NULL, obs_ids) - ), + .pedigreeMeanMatrix(fsize, obs_ids, "meanLI"), OpenMx::mxAlgebraFromString(algebra_str, - name = "V", dimnames = list(obs_ids, obs_ids) + name = "V", dimnames = list(obs_ids, obs_ids) ), OpenMx::mxExpectationNormal(covariance = "V", means = "M"), OpenMx::mxFitFunctionML() @@ -213,6 +303,168 @@ buildOneFamilyGroup <- function( do.call(OpenMx::mxModel, model_args) } +#' Build one grouped static family model +#' +#' This function constructs one OpenMx model for multiple independent family +#' observations that share the same pedigree structure, relatedness matrices, +#' variable ordering, expected covariance matrix, and expected mean vector. +#' +#' The observed data contain one family per row. The shared matrices, covariance +#' algebra, expectation, and fit function are stored only once rather than once +#' per family. +#' +#' @param group_name Name of the grouped family model. +#' @param dat A matrix or data frame where each row represents an independent +#' family and columns correspond to pedigree positions. +#' @param obs_ids A character vector of individual IDs corresponding to the +#' columns of \code{dat} and the rows/columns of the relatedness matrices. +#' @inheritParams buildOneFamilyGroup +#' @return An OpenMx model containing all static family observations. +#' @keywords internal +.buildGroupedStaticFamily <- function( + group_name, + dat, + obs_ids, + Addmat = NULL, + Nucmat = NULL, + Extmat = NULL, + Mtdmat = NULL, + Amimat = NULL, + Dmgmat = NULL, + condenseMatrixSlots = TRUE, + clean_ids = FALSE +) { + .require_openmx(".buildGroupedStaticFamily") + + if (clean_ids) { + obs_ids <- make_clean_personids(obs_ids) + # clean once + clean_ids <- FALSE + } + + dat <- as.data.frame(dat, check.names = FALSE) + + if (nrow(dat) < 1L) { + stop("'dat' must contain at least one family row.") + } + + if (ncol(dat) != length(obs_ids)) { + stop("The number of columns in 'dat' must equal the length of 'obs_ids'.") + } + + if (anyDuplicated(obs_ids)) { + stop("'obs_ids' must be unique.") + } + + colnames(dat) <- obs_ids + + # Determine family size from first available matrix. + fsize <- .pedigreeFamilySize(list(Addmat, Dmgmat, Nucmat, Extmat, Mtdmat, Amimat)) + + if (length(obs_ids) != fsize) { + stop("Length of 'obs_ids' must equal the shared family size.") + } + + # If Extmat is requested but not supplied as a matrix, create a unit matrix + # (all members share the extended environment equally). + if (!is.null(Extmat) && !is.matrix(Extmat)) { + Extmat <- matrix(1, nrow = fsize, ncol = fsize) + } + + # This is the same static variance-component specification used by + # buildOneFamilyGroup(), but it is constructed only once for all rows of dat. + mat_spec <- list( + list( + mat = Addmat, + mxname = "A", + term = "(A %x% ModelOne.Vad)" + ), + list( + mat = Dmgmat, + mxname = "D", + term = "(D %x% ModelOne.Vdd)" + ), + list( + mat = Nucmat, + mxname = "Cn", + term = "(Cn %x% ModelOne.Vcn)" + ), + list( + mat = Extmat, + mxname = "Ce", + term = "(Ce %x% ModelOne.Vce)" + ), + list( + mat = Amimat, + mxname = "Am", + term = "(Am %x% ModelOne.Vam)" + ), + list( + mat = Mtdmat, + mxname = "Mt", + term = "(Mt %x% ModelOne.Vmt)" + ) + ) + + active <- Filter(function(s) !is.null(s$mat), mat_spec) + + # Build one copy of each shared relatedness matrix. + relmat_list <- lapply(active, function(s) { + .pedigreeRelatednessMatrix( + s$mat, + fsize, + s$mxname, + condense = condenseMatrixSlots, + symmetrize = FALSE + ) + }) + + # Add one identity matrix for the unique environment. + mat_list <- c( + list(OpenMx::mxMatrix("Iden", nrow = fsize, ncol = fsize, name = "I")), + relmat_list + ) + + algebra_terms <- vapply(active, `[[`, character(1), "term") + + # Unique environment is always included. + algebra_terms <- c(algebra_terms, "(I %x% ModelOne.Ver)") + + algebra_str <- paste(algebra_terms, collapse = " + ") + + # The full data frame is passed to one raw-data model. Each row is an + # independent family likelihood contribution under the shared M and V. + model_args <- c( + list(name = group_name), + mat_list, + list( + OpenMx::mxData( + observed = dat, + type = "raw", + sort = FALSE + ), + .pedigreeMeanMatrix( + fsize, + obs_ids, + "meanLI" + ), + OpenMx::mxAlgebraFromString( + algebra_str, + name = "V", + dimnames = list(obs_ids, obs_ids) + ), + OpenMx::mxExpectationNormal( + covariance = "V", + means = "M", + dimnames = obs_ids + ), + OpenMx::mxFitFunctionML() + ) + ) + + do.call(OpenMx::mxModel, model_args) +} + #' Build family group models #' #' This function constructs OpenMx models for multiple family groups based on @@ -234,10 +486,34 @@ buildFamilyGroups <- function( Amimat = NULL, Dmgmat = NULL, prefix = "fam", - condenseMatrixSlots = TRUE + condenseMatrixSlots = TRUE, + clean_ids = TRUE, + group_static_families = FALSE ) { .require_openmx("buildFamilyGroups") + if (clean_ids == TRUE) { + obs_ids <- make_clean_personids(obs_ids) + # clean once + clean_ids <- FALSE + } + if (group_static_families) { + return(list( + .buildGroupedStaticFamily( + group_name = paste0(prefix, "_grouped"), + dat = dat, + obs_ids = obs_ids, + Addmat = Addmat, + Nucmat = Nucmat, + Extmat = Extmat, + Mtdmat = Mtdmat, + Amimat = Amimat, + Dmgmat = Dmgmat, + condenseMatrixSlots = condenseMatrixSlots, + clean_ids = clean_ids + ) + )) + } numfam <- nrow(dat) groups <- vector("list", numfam) @@ -253,7 +529,86 @@ buildFamilyGroups <- function( Dmgmat = Dmgmat, full_df_row = full_df_row, obs_ids = obs_ids, - condenseMatrixSlots = condenseMatrixSlots + condenseMatrixSlots = condenseMatrixSlots, + clean_ids = clean_ids + ) + } + + groups +} + + +#' Build family group models with per-family relatedness matrices +#' +#' This function constructs OpenMx models for multiple family groups, each with its own +#' relatedness matrices, observed IDs, and (for temporal models) birth years / historical +#' moderators. Use this when families vary in size or structure; for families that all share +#' the same relatedness matrices, see \code{\link{buildFamilyGroups}}. +#' @inheritParams buildOneFamilyGroup +#' @param dat_list A list of numeric vectors of observed data, one per family. +#' @param obs_ids_list A list of character vectors of individual IDs, one per family, matching +#' \code{dat_list} and the rows/columns of that family's relatedness matrices. +#' @param Addmat_list A list of additive genetic relatedness matrices, one per family. +#' @param Nucmat_list A list of nuclear family shared environment relatedness matrices, one per family. +#' @param Extmat_list A list of common extended family environment relatedness matrices, one per family. +#' @param Mtdmat_list A list of mitochondrial genetic relatedness matrices, one per family. +#' @param Amimat_list A list of additive by mitochondrial interaction relatedness matrices, one per family. +#' @param Dmgmat_list A list of dominance genetic relatedness matrices, one per family. +#' @param prefix A prefix for naming the family groups. Default is "fam". +#' @param birth_year_list A list of numeric birth-year vectors, one per family, each matching +#' the corresponding entry of \code{obs_ids_list}. Only used when \code{temporal = TRUE}. +#' @param H_list A list of historical-moderator matrices, one per family. Only used when +#' \code{temporal = TRUE}. +#' @return A list of OpenMx models for each family group. +#' @export + +buildFamilyGroups_list <- function( + dat_list, + obs_ids_list, + Addmat_list = NULL, + Nucmat_list = NULL, + Extmat_list = NULL, + Mtdmat_list = NULL, + Amimat_list = NULL, + Dmgmat_list = NULL, + prefix = "fam", + condenseMatrixSlots = TRUE, + clean_ids = TRUE +) { + .require_openmx("buildFamilyGroups_list") + + numfam <- length(dat_list) + groups <- vector("list", numfam) + + get_or_null <- function(x, i) { + if (is.null(x)) NULL else x[[i]] + } + + for (afam in seq_len(numfam)) { + if (clean_ids == TRUE) { + obs_ids <- make_clean_personids(obs_ids_list[[afam]]) + clean_ids <- FALSE + } else { + obs_ids <- obs_ids_list[[afam]] + } + full_df_row <- matrix( + dat_list[[afam]], + nrow = 1, + dimnames = list(NULL, obs_ids) + ) + + groups[[afam]] <- buildOneFamilyGroup( + group_name = paste0(prefix, afam), + Addmat = get_or_null(Addmat_list, afam), + Nucmat = get_or_null(Nucmat_list, afam), + Extmat = get_or_null(Extmat_list, afam), + Mtdmat = get_or_null(Mtdmat_list, afam), + Amimat = get_or_null(Amimat_list, afam), + Dmgmat = get_or_null(Dmgmat_list, afam), + full_df_row = full_df_row, + obs_ids = obs_ids, + condenseMatrixSlots = condenseMatrixSlots, + clean_ids = clean_ids # already cleaned above if requested ) } @@ -271,12 +626,15 @@ buildFamilyGroups <- function( #' @param vars A named list or vector of initial variance component values. #' @param group_models A list of OpenMx models for each family group. #' @param ci Logical. If TRUE, include confidence interval computations for the variance components. Default is FALSE +#' @param components Character vector of component keys to include (any of "a", "d", "cn", +#' "ce", "mt", "am", "e"). Only used when \code{temporal = TRUE}. Default is \code{c("a", "e")}. #' @return An OpenMx pedigree model combining variance components and family groups. #' @export buildPedigreeMx <- function(model_name, vars, group_models, ci = FALSE, - condenseMatrixSlots = TRUE) { + condenseMatrixSlots = TRUE, + components = c("a", "e")) { .require_openmx("buildPedigreeMx") group_names <- vapply(group_models, function(m) m$name, character(1)) @@ -306,26 +664,24 @@ buildPedigreeMx <- function(model_name, vars, group_models, flags <- lapply(vc_map, function(pat) grepl(pat, all_formulas, fixed = TRUE)) - OpenMx::mxModel( - model_name, - buildPedigreeModelCovariance( - vars, - Vad = isTRUE(flags$Vad), - Vdd = isTRUE(flags$Vdd), - Vcn = isTRUE(flags$Vcn), - Vce = isTRUE(flags$Vce), - Vmt = isTRUE(flags$Vmt), - Vam = isTRUE(flags$Vam), - Ver = isTRUE(flags$Ver) - ), - group_models, - OpenMx::mxFitFunctionMultigroup(group_names), - ci = if (ci & any(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)) { - OpenMx::mxCI(c("vad", "vdd", "vcn", "vce", "vmt", "vam", "ver")[c(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)]) - } else { - NULL - } + model_one <- buildPedigreeModelCovariance( + vars, + Vad = isTRUE(flags$Vad), + Vdd = isTRUE(flags$Vdd), + Vcn = isTRUE(flags$Vcn), + Vce = isTRUE(flags$Vce), + Vmt = isTRUE(flags$Vmt), + Vam = isTRUE(flags$Vam), + Ver = isTRUE(flags$Ver) ) + + ci_obj <- if (ci & any(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)) { + OpenMx::mxCI(c("vad", "vdd", "vcn", "vce", "vmt", "vam", "ver")[c(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)]) + } else { + NULL + } + + .assemblePedigreeMx(model_name, model_one, group_models, ci_obj) } #' Fit an OpenMx pedigree model to observed data @@ -406,18 +762,46 @@ fitPedigreeModel <- function( ) if (runmodel == TRUE) { if (tryhard == TRUE) { - fitted_model <- OpenMx::mxTryHard(pedigree_model, silent = TRUE, extraTries = extraTries, intervals = intervals) + return_model <- OpenMx::mxTryHard(pedigree_model, silent = TRUE, extraTries = extraTries, intervals = intervals) } else { - fitted_model <- OpenMx::mxRun(pedigree_model, intervals = intervals) + return_model <- OpenMx::mxRun(pedigree_model, intervals = intervals) } } else { - fitted_model <- pedigree_model + return_model <- pedigree_model } - fitted_model + return_model } +#' Assemble the top-level pedigree mxModel (internal) +#' +#' Combines the covariance sub-model (named \code{"ModelOne"}), the family-group models, +#' the multigroup fit function, and (optionally) a confidence-interval specification into +#' one mxModel. Shared by both the static and temporal branches of +#' \code{\link{buildPedigreeMx}}, which otherwise only differ in how \code{model_one} +#' and \code{ci_obj} are built. +#' +#' @param model_name Name of the overall pedigree model. +#' @param model_one The covariance sub-model (from \code{\link{buildPedigreeModelCovariance}}). +#' @param group_models A list of OpenMx models for each family group. +#' @param ci_obj An \code{mxCI} object to include, or NULL to omit confidence intervals. +#' @return An OpenMx pedigree model. +#' @keywords internal +.assemblePedigreeMx <- function(model_name, model_one, group_models, ci_obj = NULL) { + group_names <- vapply(group_models, function(m) m$name, character(1)) + + model_parts <- c( + list(model_name), + list(model_one), + group_models, + list(OpenMx::mxFitFunctionMultigroup(group_names)) + ) + if (!is.null(ci_obj)) model_parts[[length(model_parts) + 1]] <- ci_obj + + do.call(OpenMx::mxModel, model_parts) +} + #' Align Phenotype Vector to Matrix Format for OpenMx #' #' This function takes a pedigree data frame, a specified phenotype column, and a vector of IDs to keep, and returns a matrix formatted for use in OpenMx models. The resulting matrix has one row and columns corresponding to the specified IDs, with values taken from the phenotype column of the pedigree. @@ -455,3 +839,15 @@ condenseMatrixSlots <- function(model) { # } OpenMx::imxConDecMatrixSlots(model) } + +#' Make Clean IDs for OpenMx +#' +#' This function takes a vector of IDs and returns a cleaned version suitable for use in OpenMx models. It replaces any illegal characters (such as '.') with underscores and ensures that the IDs are valid R variable names. This is important because OpenMx does not allow certain characters in matrix or variable names, which can lead to errors when building models. +#' @param ids A vector of IDs to be cleaned. +#' @return A vector of cleaned IDs suitable for use in OpenMx models. +#' +make_clean_personids <- function(ids) { + .require_openmx("make_clean_personids") + OpenMx::mxMakeNames(as.character(ids)) +} + diff --git a/man/buildFamilyGroups.Rd b/man/buildFamilyGroups.Rd index fd5a4f3a..bf67c351 100644 --- a/man/buildFamilyGroups.Rd +++ b/man/buildFamilyGroups.Rd @@ -14,7 +14,9 @@ buildFamilyGroups( Amimat = NULL, Dmgmat = NULL, prefix = "fam", - condenseMatrixSlots = TRUE + condenseMatrixSlots = TRUE, + clean_ids = TRUE, + group_static_families = FALSE ) } \arguments{ diff --git a/man/buildFamilyGroups_list.Rd b/man/buildFamilyGroups_list.Rd new file mode 100644 index 00000000..1dd236c5 --- /dev/null +++ b/man/buildFamilyGroups_list.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{buildFamilyGroups_list} +\alias{buildFamilyGroups_list} +\title{Build family group models with per-family relatedness matrices} +\usage{ +buildFamilyGroups_list( + dat_list, + obs_ids_list, + Addmat_list = NULL, + Nucmat_list = NULL, + Extmat_list = NULL, + Mtdmat_list = NULL, + Amimat_list = NULL, + Dmgmat_list = NULL, + prefix = "fam", + condenseMatrixSlots = TRUE, + clean_ids = TRUE +) +} +\arguments{ +\item{dat_list}{A list of numeric vectors of observed data, one per family.} + +\item{obs_ids_list}{A list of character vectors of individual IDs, one per family, matching +\code{dat_list} and the rows/columns of that family's relatedness matrices.} + +\item{Addmat_list}{A list of additive genetic relatedness matrices, one per family.} + +\item{Nucmat_list}{A list of nuclear family shared environment relatedness matrices, one per family.} + +\item{Extmat_list}{A list of common extended family environment relatedness matrices, one per family.} + +\item{Mtdmat_list}{A list of mitochondrial genetic relatedness matrices, one per family.} + +\item{Amimat_list}{A list of additive by mitochondrial interaction relatedness matrices, one per family.} + +\item{Dmgmat_list}{A list of dominance genetic relatedness matrices, one per family.} + +\item{prefix}{A prefix for naming the family groups. Default is "fam".} + +\item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} + +\item{birth_year_list}{A list of numeric birth-year vectors, one per family, each matching +the corresponding entry of \code{obs_ids_list}. Only used when \code{temporal = TRUE}.} + +\item{H_list}{A list of historical-moderator matrices, one per family. Only used when +\code{temporal = TRUE}.} +} +\value{ +A list of OpenMx models for each family group. +} +\description{ +This function constructs OpenMx models for multiple family groups, each with its own +relatedness matrices, observed IDs, and (for temporal models) birth years / historical +moderators. Use this when families vary in size or structure; for families that all share +the same relatedness matrices, see \code{\link{buildFamilyGroups}}. +} diff --git a/man/buildOneFamilyGroup.Rd b/man/buildOneFamilyGroup.Rd index d9cf6b02..1cf09e62 100644 --- a/man/buildOneFamilyGroup.Rd +++ b/man/buildOneFamilyGroup.Rd @@ -14,7 +14,9 @@ buildOneFamilyGroup( Dmgmat = NULL, full_df_row, obs_ids, - condenseMatrixSlots = TRUE + symmetrize = FALSE, + condenseMatrixSlots = TRUE, + clean_ids = FALSE ) } \arguments{ diff --git a/man/buildPedigreeMx.Rd b/man/buildPedigreeMx.Rd index 69f07ee1..f01a66b0 100644 --- a/man/buildPedigreeMx.Rd +++ b/man/buildPedigreeMx.Rd @@ -9,7 +9,8 @@ buildPedigreeMx( vars, group_models, ci = FALSE, - condenseMatrixSlots = TRUE + condenseMatrixSlots = TRUE, + components = c("a", "e") ) } \arguments{ @@ -22,6 +23,9 @@ buildPedigreeMx( \item{ci}{Logical. If TRUE, include confidence interval computations for the variance components. Default is FALSE} \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} + +\item{components}{Character vector of component keys to include (any of "a", "d", "cn", +"ce", "mt", "am", "e"). Only used when \code{temporal = TRUE}. Default is \code{c("a", "e")}.} } \value{ An OpenMx pedigree model combining variance components and family groups. diff --git a/man/dot-assemblePedigreeMx.Rd b/man/dot-assemblePedigreeMx.Rd new file mode 100644 index 00000000..cdb88cdf --- /dev/null +++ b/man/dot-assemblePedigreeMx.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{.assemblePedigreeMx} +\alias{.assemblePedigreeMx} +\title{Assemble the top-level pedigree mxModel (internal)} +\usage{ +.assemblePedigreeMx(model_name, model_one, group_models, ci_obj = NULL) +} +\arguments{ +\item{model_name}{Name of the overall pedigree model.} + +\item{model_one}{The covariance sub-model (from \code{\link{buildPedigreeModelCovariance}}).} + +\item{group_models}{A list of OpenMx models for each family group.} + +\item{ci_obj}{An \code{mxCI} object to include, or NULL to omit confidence intervals.} +} +\value{ +An OpenMx pedigree model. +} +\description{ +Combines the covariance sub-model (named \code{"ModelOne"}), the family-group models, +the multigroup fit function, and (optionally) a confidence-interval specification into +one mxModel. Shared by both the static and temporal branches of +\code{\link{buildPedigreeMx}}, which otherwise only differ in how \code{model_one} +and \code{ci_obj} are built. +} +\keyword{internal} diff --git a/man/dot-buildGroupedStaticFamily.Rd b/man/dot-buildGroupedStaticFamily.Rd new file mode 100644 index 00000000..87a23e50 --- /dev/null +++ b/man/dot-buildGroupedStaticFamily.Rd @@ -0,0 +1,60 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{.buildGroupedStaticFamily} +\alias{.buildGroupedStaticFamily} +\title{Build one grouped static family model} +\usage{ +.buildGroupedStaticFamily( + group_name, + dat, + obs_ids, + Addmat = NULL, + Nucmat = NULL, + Extmat = NULL, + Mtdmat = NULL, + Amimat = NULL, + Dmgmat = NULL, + condenseMatrixSlots = TRUE, + clean_ids = FALSE +) +} +\arguments{ +\item{group_name}{Name of the grouped family model.} + +\item{dat}{A matrix or data frame where each row represents an independent +family and columns correspond to pedigree positions.} + +\item{obs_ids}{A character vector of individual IDs corresponding to the +columns of \code{dat} and the rows/columns of the relatedness matrices.} + +\item{Addmat}{Additive genetic relatedness matrix (from \code{\link{ped2add}}).} + +\item{Nucmat}{Nuclear family shared environment relatedness matrix (from \code{\link{ped2cn}}).} + +\item{Extmat}{Common extended family environment relatedness matrix. When non-NULL, +a Vce term scaled by this matrix is added to the covariance. If a non-matrix +value (e.g. \code{TRUE}) is supplied, a unit matrix (all members share equally) +is created automatically.} + +\item{Mtdmat}{Mitochondrial genetic relatedness matrix (from \code{\link{ped2mit}}).} + +\item{Amimat}{Additive by mitochondrial interaction relatedness matrix.} + +\item{Dmgmat}{Dominance genetic relatedness matrix.} + +\item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} +} +\value{ +An OpenMx model containing all static family observations. +} +\description{ +This function constructs one OpenMx model for multiple independent family +observations that share the same pedigree structure, relatedness matrices, +variable ordering, expected covariance matrix, and expected mean vector. +} +\details{ +The observed data contain one family per row. The shared matrices, covariance +algebra, expectation, and fit function are stored only once rather than once +per family. +} +\keyword{internal} diff --git a/man/dot-pedigreeFamilySize.Rd b/man/dot-pedigreeFamilySize.Rd new file mode 100644 index 00000000..6287c600 --- /dev/null +++ b/man/dot-pedigreeFamilySize.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{.pedigreeFamilySize} +\alias{.pedigreeFamilySize} +\title{Determine family size from a family's relatedness matrices (internal)} +\usage{ +.pedigreeFamilySize(mats_in) +} +\arguments{ +\item{mats_in}{A list of relatedness matrices (possibly containing NULLs); the first +non-NULL matrix's row count is used as the family size.} +} +\value{ +Integer family size. +} +\description{ +Determine family size from a family's relatedness matrices (internal) +} +\keyword{internal} diff --git a/man/dot-pedigreeMeanMatrix.Rd b/man/dot-pedigreeMeanMatrix.Rd new file mode 100644 index 00000000..0fd9ceb7 --- /dev/null +++ b/man/dot-pedigreeMeanMatrix.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{.pedigreeMeanMatrix} +\alias{.pedigreeMeanMatrix} +\title{Build the free mean mxMatrix for a family group (internal)} +\usage{ +.pedigreeMeanMatrix(fsize, obs_ids, label) +} +\arguments{ +\item{fsize}{Family size.} + +\item{obs_ids}{Character vector of individual IDs.} + +\item{label}{The mean parameter's label.} +} +\value{ +An mxMatrix object. +} +\description{ +Shared by the static and temporal branches of \code{\link{buildOneFamilyGroup}}; they +only differ in the parameter label ("meanLI" vs "mean_y"). +} +\keyword{internal} diff --git a/man/dot-pedigreeRelatednessMatrix.Rd b/man/dot-pedigreeRelatednessMatrix.Rd new file mode 100644 index 00000000..dd4d3c48 --- /dev/null +++ b/man/dot-pedigreeRelatednessMatrix.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{.pedigreeRelatednessMatrix} +\alias{.pedigreeRelatednessMatrix} +\title{Build a relatedness mxMatrix (internal)} +\usage{ +.pedigreeRelatednessMatrix( + mat, + fsize, + name, + condense = TRUE, + symmetrize = FALSE +) +} +\arguments{ +\item{mat}{The relatedness matrix.} + +\item{fsize}{Family size.} + +\item{name}{The mxMatrix's name (e.g. "A", "Cn").} + +\item{condense}{Logical. If TRUE, apply \code{\link{condenseMatrixSlots}}.} + +\item{symmetrize}{Logical. If TRUE, symmetrize \code{mat} via \code{\link{make_symmetric}} +before use; if FALSE, coerce via \code{as.matrix} only.} +} +\value{ +An mxMatrix object. +} +\description{ +Builds the \code{"Symm"} mxMatrix for one relatedness matrix. +} +\keyword{internal} diff --git a/man/make_clean_personids.Rd b/man/make_clean_personids.Rd new file mode 100644 index 00000000..5b96e50d --- /dev/null +++ b/man/make_clean_personids.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildmxPedigrees.R +\name{make_clean_personids} +\alias{make_clean_personids} +\title{Make Clean IDs for OpenMx} +\usage{ +make_clean_personids(ids) +} +\arguments{ +\item{ids}{A vector of IDs to be cleaned.} +} +\value{ +A vector of cleaned IDs suitable for use in OpenMx models. +} +\description{ +This function takes a vector of IDs and returns a cleaned version suitable for use in OpenMx models. It replaces any illegal characters (such as '.') with underscores and ensures that the IDs are valid R variable names. This is important because OpenMx does not allow certain characters in matrix or variable names, which can lead to errors when building models. +} diff --git a/tests/testthat/test-buildmxPedigrees.R b/tests/testthat/test-buildmxPedigrees.R index f97227f4..1c6d0200 100644 --- a/tests/testthat/test-buildmxPedigrees.R +++ b/tests/testthat/test-buildmxPedigrees.R @@ -96,7 +96,7 @@ test_that("buildPedigreeModelCovariance works with minimal components (Vad + Ver test_that("buildOneFamilyGroup errors when no relatedness matrix is provided", { skip_if_not_installed("OpenMx") dat <- make_dat2() - expect_error( + expect_warning( buildOneFamilyGroup( group_name = "fam1", Addmat = NULL, Nucmat = NULL, Extmat = NULL, @@ -104,7 +104,7 @@ test_that("buildOneFamilyGroup errors when no relatedness matrix is provided", { full_df_row = dat, obs_ids = c("y1", "y2") ), - regexp = "At least one relatedness matrix must be provided" + regexp = "At least one relatedness matrix should be provided" ) }) From 1e9dbdd09913e6f28293c7e33497270651f48449 Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 10:43:31 -0400 Subject: [PATCH 06/11] Update addParentalChain.R --- R/addParentalChain.R | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/R/addParentalChain.R b/R/addParentalChain.R index c0cad13b..7a468250 100644 --- a/R/addParentalChain.R +++ b/R/addParentalChain.R @@ -197,12 +197,11 @@ addParentalChain <- function( } # Find all nodes reachable from this person by following paternal edges. - reachable_ids <- igraph::subcomponent( + reachable_ids <- names(igraph::subcomponent( graph = parental_graph, v = id_chr, mode = traversal_direction - ) |> - names() + )) # Remove the person themselves from their paternal ancestor list. reachable_ids <- setdiff(reachable_ids, id_chr) From 28b376d8b8f9caec7075d50c15b36b5d110230fa Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 10:47:24 -0400 Subject: [PATCH 07/11] documentation --- R/buildmxPedigrees.R | 53 +++++++++++++++-------------- R/checkParents.R | 20 +++++------ R/helpGeneric.R | 2 +- data-raw/benchmark_rowlessParents.R | 2 +- man/buildFamilyGroups.Rd | 2 ++ man/buildFamilyGroups_list.Rd | 2 ++ man/buildOneFamilyGroup.Rd | 4 +++ man/dot-buildGroupedStaticFamily.Rd | 2 ++ 8 files changed, 49 insertions(+), 38 deletions(-) diff --git a/R/buildmxPedigrees.R b/R/buildmxPedigrees.R index 6ec04664..76fc5306 100644 --- a/R/buildmxPedigrees.R +++ b/R/buildmxPedigrees.R @@ -177,6 +177,8 @@ buildPedigreeModelCovariance <- function( #' \code{full_df_row} and the rows/columns of the relatedness matrices. Must be in the #' same order as the relatedness matrix rows. #' @param condenseMatrixSlots Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE. +#' @param clean_ids Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE. +#' @param symmetrize Logical. If TRUE, symmetrize the relatedness matrices before use. Default is FALSE. #' @return An OpenMx model for the specified family group. #' @export @@ -203,7 +205,7 @@ buildOneFamilyGroup <- function( # Determine family size from first available matrix. Shared by both branches below. if ( # not any of the matrices are provided is.null(Addmat) && is.null(Dmgmat) && is.null(Nucmat) && - is.null(Extmat) && is.null(Mtdmat) && is.null(Amimat) + is.null(Extmat) && is.null(Mtdmat) && is.null(Amimat) ) { warning("At least one relatedness matrix should be provided. Using the number of columns in 'full_df_row' as family size.") fsize <- ncol(full_df_row) @@ -293,7 +295,7 @@ buildOneFamilyGroup <- function( OpenMx::mxData(observed = full_df_row, type = "raw", sort = FALSE), .pedigreeMeanMatrix(fsize, obs_ids, "meanLI"), OpenMx::mxAlgebraFromString(algebra_str, - name = "V", dimnames = list(obs_ids, obs_ids) + name = "V", dimnames = list(obs_ids, obs_ids) ), OpenMx::mxExpectationNormal(covariance = "V", means = "M"), OpenMx::mxFitFunctionML() @@ -322,17 +324,17 @@ buildOneFamilyGroup <- function( #' @return An OpenMx model containing all static family observations. #' @keywords internal .buildGroupedStaticFamily <- function( - group_name, - dat, - obs_ids, - Addmat = NULL, - Nucmat = NULL, - Extmat = NULL, - Mtdmat = NULL, - Amimat = NULL, - Dmgmat = NULL, - condenseMatrixSlots = TRUE, - clean_ids = FALSE + group_name, + dat, + obs_ids, + Addmat = NULL, + Nucmat = NULL, + Extmat = NULL, + Mtdmat = NULL, + Amimat = NULL, + Dmgmat = NULL, + condenseMatrixSlots = TRUE, + clean_ids = FALSE ) { .require_openmx(".buildGroupedStaticFamily") @@ -563,17 +565,17 @@ buildFamilyGroups <- function( #' @export buildFamilyGroups_list <- function( - dat_list, - obs_ids_list, - Addmat_list = NULL, - Nucmat_list = NULL, - Extmat_list = NULL, - Mtdmat_list = NULL, - Amimat_list = NULL, - Dmgmat_list = NULL, - prefix = "fam", - condenseMatrixSlots = TRUE, - clean_ids = TRUE + dat_list, + obs_ids_list, + Addmat_list = NULL, + Nucmat_list = NULL, + Extmat_list = NULL, + Mtdmat_list = NULL, + Amimat_list = NULL, + Dmgmat_list = NULL, + prefix = "fam", + condenseMatrixSlots = TRUE, + clean_ids = TRUE ) { .require_openmx("buildFamilyGroups_list") @@ -675,7 +677,7 @@ buildPedigreeMx <- function(model_name, vars, group_models, Ver = isTRUE(flags$Ver) ) - ci_obj <- if (ci & any(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)) { + ci_obj <- if (ci && any(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)) { OpenMx::mxCI(c("vad", "vdd", "vcn", "vce", "vmt", "vam", "ver")[c(flags$Vad, flags$Vdd, flags$Vcn, flags$Vce, flags$Vmt, flags$Vam, flags$Ver)]) } else { NULL @@ -850,4 +852,3 @@ make_clean_personids <- function(ids) { .require_openmx("make_clean_personids") OpenMx::mxMakeNames(as.character(ids)) } - diff --git a/R/checkParents.R b/R/checkParents.R index 6b32dd07..c720fdcb 100644 --- a/R/checkParents.R +++ b/R/checkParents.R @@ -111,17 +111,17 @@ checkParentIDs <- function(ped, verbose = FALSE, repair = FALSE, # Are any parents in both momID and dadID? momdad <- intersect(ped$dadID, ped$momID) momdad <- momdad[!is.na(momdad)] -if (length(momdad) > 0) { - validation_results$parents_in_both <- momdad - if (verbose == TRUE) { - cat( - "Some individuals appear in both momID and dadID roles.\n", - "These individuals are:\n", - sep = "" - ) - message(paste(momdad, collapse = ", ")) + if (length(momdad) > 0) { + validation_results$parents_in_both <- momdad + if (verbose == TRUE) { + cat( + "Some individuals appear in both momID and dadID roles.\n", + "These individuals are:\n", + sep = "" + ) + message(paste(momdad, collapse = ", ")) + } } -} if (!repair) { diff --git a/R/helpGeneric.R b/R/helpGeneric.R index 0c350777..2bb0c4e6 100644 --- a/R/helpGeneric.R +++ b/R/helpGeneric.R @@ -129,7 +129,7 @@ as_numeric_matrix <- function(x) { #' @return A numeric matrix, symmetrized if needed. #' make_symmetric <- function(x, tol = 1e-10) { - if (inherits(x, "sparseMatrix")) { + if (inherits(x, "sparseMatrix")) { if (!Matrix::isSymmetric(x, tol = tol)) { x <- (x + Matrix::t(x)) / 2 } diff --git a/data-raw/benchmark_rowlessParents.R b/data-raw/benchmark_rowlessParents.R index 3928f040..e4f09331 100644 --- a/data-raw/benchmark_rowlessParents.R +++ b/data-raw/benchmark_rowlessParents.R @@ -130,7 +130,7 @@ df_plot <- benchmark_results %>% mutate( ) ~ "big" ), method = case_when( - expr %in% c("base_small", "base_big") ~ "base", + expr %in% c("base_small", "base_big") ~ "base", expr %in% c("rows_small", "rows_big") ~ "rows", expr %in% c("schur_small", "schur_big") ~ "schur" ) # make base the reference level for the linear model, so that the intercept is the mean of the base method diff --git a/man/buildFamilyGroups.Rd b/man/buildFamilyGroups.Rd index bf67c351..7242ee48 100644 --- a/man/buildFamilyGroups.Rd +++ b/man/buildFamilyGroups.Rd @@ -43,6 +43,8 @@ is created automatically.} \item{prefix}{A prefix for naming the family groups. Default is "fam".} \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} + +\item{clean_ids}{Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE.} } \value{ A list of OpenMx models for each family group. diff --git a/man/buildFamilyGroups_list.Rd b/man/buildFamilyGroups_list.Rd index 1dd236c5..f27ac18e 100644 --- a/man/buildFamilyGroups_list.Rd +++ b/man/buildFamilyGroups_list.Rd @@ -40,6 +40,8 @@ buildFamilyGroups_list( \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} +\item{clean_ids}{Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE.} + \item{birth_year_list}{A list of numeric birth-year vectors, one per family, each matching the corresponding entry of \code{obs_ids_list}. Only used when \code{temporal = TRUE}.} diff --git a/man/buildOneFamilyGroup.Rd b/man/buildOneFamilyGroup.Rd index 1cf09e62..2800d569 100644 --- a/man/buildOneFamilyGroup.Rd +++ b/man/buildOneFamilyGroup.Rd @@ -43,7 +43,11 @@ is created automatically.} \code{full_df_row} and the rows/columns of the relatedness matrices. Must be in the same order as the relatedness matrix rows.} +\item{symmetrize}{Logical. If TRUE, symmetrize the relatedness matrices before use. Default is FALSE.} + \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} + +\item{clean_ids}{Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE.} } \value{ An OpenMx model for the specified family group. diff --git a/man/dot-buildGroupedStaticFamily.Rd b/man/dot-buildGroupedStaticFamily.Rd index 87a23e50..daada573 100644 --- a/man/dot-buildGroupedStaticFamily.Rd +++ b/man/dot-buildGroupedStaticFamily.Rd @@ -43,6 +43,8 @@ is created automatically.} \item{Dmgmat}{Dominance genetic relatedness matrix.} \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} + +\item{clean_ids}{Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE.} } \value{ An OpenMx model containing all static family observations. From 8d609b2a157a027057899adfa2a26db661a65f19 Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 11:19:29 -0400 Subject: [PATCH 08/11] missed documentation --- R/buildmxPedigrees.R | 7 ++++++- man/buildFamilyGroups.Rd | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/R/buildmxPedigrees.R b/R/buildmxPedigrees.R index 76fc5306..a412c0f3 100644 --- a/R/buildmxPedigrees.R +++ b/R/buildmxPedigrees.R @@ -476,6 +476,7 @@ buildOneFamilyGroup <- function( #' @param obs_ids A character vector of individual IDs corresponding to the columns of \code{dat} #' and the rows/columns of the relatedness matrices. #' @param prefix A prefix for naming the family groups. Default is "fam". +#' @param group_static_families Logical. If TRUE, build one grouped static family model instead of separate models for each family. Default is FALSE. #' @return A list of OpenMx models for each family group. #' @export @@ -683,7 +684,11 @@ buildPedigreeMx <- function(model_name, vars, group_models, NULL } - .assemblePedigreeMx(model_name, model_one, group_models, ci_obj) + .assemblePedigreeMx( + model_name = model_name, + model_one = model_one, + group_models = group_models, + ci_obj = ci_obj) } #' Fit an OpenMx pedigree model to observed data diff --git a/man/buildFamilyGroups.Rd b/man/buildFamilyGroups.Rd index 7242ee48..1a7e3d3d 100644 --- a/man/buildFamilyGroups.Rd +++ b/man/buildFamilyGroups.Rd @@ -45,6 +45,8 @@ is created automatically.} \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} \item{clean_ids}{Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE.} + +\item{group_static_families}{Logical. If TRUE, build one grouped static family model instead of separate models for each family. Default is FALSE.} } \value{ A list of OpenMx models for each family group. From 461899e6e32ab84f69e1912ea19d1f1602bb818b Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 13:43:06 -0400 Subject: [PATCH 09/11] Refactor .pedigreeRelatednessMatrix for matrix handling Updated the .pedigreeRelatednessMatrix function to ensure that the matrix is always coerced to a base dense matrix after symmetrization. Added error handling for matrix creation. --- R/buildmxPedigrees.R | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/R/buildmxPedigrees.R b/R/buildmxPedigrees.R index a412c0f3..6b53312e 100644 --- a/R/buildmxPedigrees.R +++ b/R/buildmxPedigrees.R @@ -122,16 +122,21 @@ buildPedigreeModelCovariance <- function( #' @keywords internal .pedigreeRelatednessMatrix <- function(mat, fsize, name, condense = TRUE, symmetrize = FALSE) { if (is.null(mat)) stop("Relatedness matrix cannot be NULL.") - # keep dense if dense, or sparse if sparse, but symmetrize if requested - if (inherits(mat, "Matrix")) { - values <- if (symmetrize) make_symmetric(mat) else mat - } else { - values <- if (symmetrize) make_symmetric(mat) else as.matrix(mat) - } - m <- OpenMx::mxMatrix( + # Symmetrize if requested, then coerce to a base dense matrix. OpenMx::mxMatrix() + # accepts only a scalar, vector, or base R matrix for 'values'; a Matrix-package + # sparse object (e.g. the dsCMatrix returned by ped2add(sparse = TRUE)) is rejected + # by matrixCheckArgument(), so it must be densified here. + values <- if (symmetrize) make_symmetric(mat) else mat + values <- as.matrix(values) + #todo allow this to be sparse and use sparse algebra in OpenMx + m <- tryCatch( OpenMx::mxMatrix( type = "Symm", nrow = fsize, ncol = fsize, free = FALSE, values = values, name = name - ) + ), error = function(e) { + print(values) + stop("Error creating mxMatrix for ", name, ": ", e$message) + }) + if (condense) m <- condenseMatrixSlots(m) m } @@ -575,6 +580,8 @@ buildFamilyGroups_list <- function( Amimat_list = NULL, Dmgmat_list = NULL, prefix = "fam", + birth_year_list = NULL, + H_list = NULL, condenseMatrixSlots = TRUE, clean_ids = TRUE ) { From 0be0c1f63ec08809292bb3b4bb47af05baadc4a5 Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 13:57:34 -0400 Subject: [PATCH 10/11] fix warning about paramets --- R/buildmxPedigrees.R | 8 ++------ man/buildFamilyGroups_list.Rd | 6 ------ tests/testthat/test-rowlessParents.R | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/R/buildmxPedigrees.R b/R/buildmxPedigrees.R index 6b53312e..704ca3a2 100644 --- a/R/buildmxPedigrees.R +++ b/R/buildmxPedigrees.R @@ -184,6 +184,7 @@ buildPedigreeModelCovariance <- function( #' @param condenseMatrixSlots Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE. #' @param clean_ids Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE. #' @param symmetrize Logical. If TRUE, symmetrize the relatedness matrices before use. Default is FALSE. + #' @return An OpenMx model for the specified family group. #' @export @@ -325,6 +326,7 @@ buildOneFamilyGroup <- function( #' family and columns correspond to pedigree positions. #' @param obs_ids A character vector of individual IDs corresponding to the #' columns of \code{dat} and the rows/columns of the relatedness matrices. +#' #' @inheritParams buildOneFamilyGroup #' @return An OpenMx model containing all static family observations. #' @keywords internal @@ -563,10 +565,6 @@ buildFamilyGroups <- function( #' @param Amimat_list A list of additive by mitochondrial interaction relatedness matrices, one per family. #' @param Dmgmat_list A list of dominance genetic relatedness matrices, one per family. #' @param prefix A prefix for naming the family groups. Default is "fam". -#' @param birth_year_list A list of numeric birth-year vectors, one per family, each matching -#' the corresponding entry of \code{obs_ids_list}. Only used when \code{temporal = TRUE}. -#' @param H_list A list of historical-moderator matrices, one per family. Only used when -#' \code{temporal = TRUE}. #' @return A list of OpenMx models for each family group. #' @export @@ -580,8 +578,6 @@ buildFamilyGroups_list <- function( Amimat_list = NULL, Dmgmat_list = NULL, prefix = "fam", - birth_year_list = NULL, - H_list = NULL, condenseMatrixSlots = TRUE, clean_ids = TRUE ) { diff --git a/man/buildFamilyGroups_list.Rd b/man/buildFamilyGroups_list.Rd index f27ac18e..c708a66e 100644 --- a/man/buildFamilyGroups_list.Rd +++ b/man/buildFamilyGroups_list.Rd @@ -41,12 +41,6 @@ buildFamilyGroups_list( \item{condenseMatrixSlots}{Logical. If TRUE, use the mxCondenseMatrixSlots wrapper to optimize memory usage for large matrices. Default is TRUE.} \item{clean_ids}{Logical. If TRUE, clean the \code{obs_ids} using \code{\link{make_clean_personids}}. Default is FALSE.} - -\item{birth_year_list}{A list of numeric birth-year vectors, one per family, each matching -the corresponding entry of \code{obs_ids_list}. Only used when \code{temporal = TRUE}.} - -\item{H_list}{A list of historical-moderator matrices, one per family. Only used when -\code{temporal = TRUE}.} } \value{ A list of OpenMx models for each family group. diff --git a/tests/testthat/test-rowlessParents.R b/tests/testthat/test-rowlessParents.R index 4a9dec5f..f467a6fc 100644 --- a/tests/testthat/test-rowlessParents.R +++ b/tests/testthat/test-rowlessParents.R @@ -19,7 +19,7 @@ test_that("ped2add warns when momID/dadID reference parents with no row", { expect_warning( ped2add(ped_rowless, sparse = FALSE), "no matching row" - ) + ) |> suppressWarnings() # }) test_that("repair_rowless_parents = TRUE fixes diagonal and sibling relatedness without warning, and does not grow the returned matrix", { From 9b7d43fb67a066bacd279e5620fb4ebb299d92ff Mon Sep 17 00:00:00 2001 From: Mason Garrison Date: Sun, 12 Jul 2026 17:51:52 -0400 Subject: [PATCH 11/11] Update test-helpGeneric.R --- tests/testthat/test-helpGeneric.R | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/testthat/test-helpGeneric.R b/tests/testthat/test-helpGeneric.R index 20097167..e5e73da7 100644 --- a/tests/testthat/test-helpGeneric.R +++ b/tests/testthat/test-helpGeneric.R @@ -67,3 +67,37 @@ test_that("functions issue a deprecation warning", { expect_warning(related_coef(), "deprecated") expect_warning(relatedness(obsR = .5), "deprecated") }) + +test_that("as_numeric_matrix coerces a data.frame to a numeric matrix", { + df <- data.frame(a = c(1L, 2L), b = c(3L, 4L)) + result <- as_numeric_matrix(df) + expect_true(is.matrix(result)) + expect_equal(storage.mode(result), "double") + expect_equal(unname(result[1, 1]), 1) +}) + +test_that("as_numeric_matrix passes through an already-numeric matrix", { + m <- matrix(c(1.5, 2.5, 3.5, 4.5), nrow = 2) + result <- as_numeric_matrix(m) + expect_equal(result, m) +}) + +test_that("make_symmetric leaves an already-symmetric matrix unchanged", { + m <- matrix(c(1, 0.5, 0.5, 1), nrow = 2) + expect_equal(make_symmetric(m), m) +}) + +test_that("make_symmetric averages a matrix with its transpose when asymmetric", { + m <- matrix(c(1, 0.4, 0.6, 1), nrow = 2) + result <- make_symmetric(m) + expect_equal(result[1, 2], 0.5) + expect_equal(result[2, 1], 0.5) + expect_true(isSymmetric(result)) +}) + +test_that("make_symmetric treats differences within tol as already symmetric", { + m <- matrix(c(1, 0.5, 0.5 + 1e-12, 1), nrow = 2) + result <- make_symmetric(m) + # Below tolerance: returned as-is, not averaged + expect_equal(result[1, 2], m[1, 2]) +})