Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tweak_initial_estimates: matrix handling #656

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 272 additions & 2 deletions R/initial-estimates.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ get_initial_est <- function(.mod, flag_fixed = FALSE){
test_nmrec_version(.min_version = "0.4.0")
check_model_object(.mod, "bbi_nonmem_model")


ctl <- nmrec::read_ctl(get_model_path(.mod))

if (isTRUE(using_old_priors(ctl))) {
Expand All @@ -105,7 +104,6 @@ get_initial_est <- function(.mod, flag_fixed = FALSE){
))
}


# Set flags
mark_flags <- if(isTRUE(flag_fixed)) "fix" else NULL

Expand All @@ -114,6 +112,11 @@ get_initial_est <- function(.mod, flag_fixed = FALSE){
omega_inits <- nmrec::extract_omega(ctl, mark_flags = mark_flags)
sigma_inits <- nmrec::extract_sigma(ctl, mark_flags = mark_flags)

# Get matrix types and options - assign as attributes
mat_opts <- get_matrix_opts(.mod)
attr(omega_inits, "mat_opts") <- mat_opts %>% filter(.data$record_type == "omega")
attr(sigma_inits, "mat_opts") <- mat_opts %>% filter(.data$record_type == "sigma")

# Format as list
inits <- list(
thetas = theta_inits,
Expand Down Expand Up @@ -186,6 +189,223 @@ matrix_to_df <- function(mat, type = c("omega","sigma")) {
}


#' Get options for all matrix-type records
#'
#' Returns a tibble indicating various specifications for matrix-type records.
#'
#' @details
#' This function returns the value types for both diagonal (variance vs
#' standard deviation) and off-diagonal (covariance vs correlation) options. If
#' cholesky decomposition was used, it will show up as under both the diagonal
#' and off diagonal columns. These options are used with `mod_matrix()` to ensure
#' the matrices are in the correct form before checking for positive-definiteness.
#'
#' This function also tabulates `SAME` specifications, matrix classes ("block" vs
#' "diagonal"), and the subtype ('plain', 'vpair', or 'values'). These options
#' are primarily used in `check_and_modify_pd()`, `expand_value_matrix()`, and
#' `cat_mat_diag()`. For more information, see the column definitions below.
#'
#' ### Column Definitions
#' - **`record_type`**: Either "sigma" or "omega".
#' - **`record_number`**: Order of occurrence in control stream file.
#' - **`mat_class`**: Either "block" or "diagonal".
#' - **`record_size`**: Size of the record. Would be `n` for a `n x n` matrix.
#' - **`subtype`**: Matrix subtype. Either 'plain', 'vpair', or 'values'.
#' - `vpair`: Parameter options represent `VALUES(diag,odiag)` pair.
#' - `values`: Parameter options contain the form `(0.01)x2 0.1`.
#' - `plain`: Parameter options reflect a typical block or diagonal matrix.
#' - **`same`**: Whether `'SAME'` was used in the matrix-type record.
#' - **`same_n`**: The number of times to duplicate the previous record.
#' - **`param_x`**: Only used when `subtype = 'values'`. Denotes the number of
#' duplicates for each specified value.
#' - For example, for the record `'(0.01)x2 0.1'`, `param_x` would be the
#' vector `c(2, 1)`, since the first value repeats twice.
#' - **`diag`**: variance vs standard deviation, or cholesky.
#' - **`off_diag`**: covariance vs correlation, or cholesky.
#'
#' ### Example:
#' ```
#' > get_matrix_opts(.mod)
#' # A tibble: 5 × 10
#' record_type record_number mat_class record_size subtype same same_n param_x diag off_diag
#' <chr> <chr> <chr> <int> <chr> <lgl> <lgl> <list> <chr> <chr>
#' 1 omega 1 block 2 plain FALSE NA <int [3]> variance covariance
#' 2 omega 2 block 2 plain FALSE NA <int [3]> standard covariance
#' 3 omega 3 block 2 plain FALSE NA <int [3]> standard correlation
#' 4 omega 4 block 2 plain FALSE NA <int [3]> variance correlation
#' 5 omega 5 block 2 plain FALSE NA <int [3]> cholesky cholesky
#' 6 sigma 1 diagonal 1 plain FALSE NA <int [1]> variance covariance
#' ```
#'
#' @inheritParams initial_estimates
#'
#' @keywords internal
get_matrix_opts <- function(.mod){

check_model_object(.mod, "bbi_nonmem_model")
ctl <- nmrec::read_ctl(get_model_path(.mod))

# Function to grab flags
get_flag_opts <- function(rec){
purrr::keep(rec$values, function(opt){
inherits(opt, "nmrec_option_flag") &&
!inherits(opt, "nmrec_option_record_name")
})
}

# Handling for nested options (diagonal matrix-type records)
get_nested <- function(rec){
purrr::keep(rec$values, function(opt){
inherits(opt, "nmrec_option_nested")
})
}

extract_mat_opts <- function(ctl, type = c("omega", "sigma")){
type <- match.arg(type)
recs <- nmrec::select_records(records = ctl, name = type)

# Matrix classes and subtypes
mat_specs <- get_matrix_types(recs)

# Handling if record type doesn't exist
if(!length(recs)){
return(
tibble::tibble(
record_type = type, record_number = NA, mat_class = NA,
record_size = NA, "diag" = NA, "off_diag" = NA) %>%
dplyr::left_join(mat_specs, by = c("record_number", "mat_class")) %>%
dplyr::relocate(c("diag", "off_diag"), .after = dplyr::everything())
)
}

# Get record sizes
if(type == "omega"){
inits <- nmrec::extract_omega(ctl)
}else{
inits <- nmrec::extract_sigma(ctl)
}
record_sizes <- attr(inits, "nmrec_record_size")

# Tabulate matrix format (diagonal and off-diagonal options)
purrr::imap_dfr(recs, function(rec, rec_num){
mat_class <- mat_specs$mat_class[rec_num]
if(mat_class == "block"){
rec_flags <- get_flag_opts(rec)
}else if(mat_class == "diagonal"){
rec$parse()
nested_recs <- get_nested(rec)
rec_flags <- unlist(purrr::map(nested_recs, get_flag_opts))
}

rec_flag_names <- purrr::map_chr(rec_flags, function(rec_flag){
rec_flag$name
})

# If no flags found, return defaults
if(length(rec_flag_names) == 0){
mat_opts <- c("diag" = "variance", "off_diag" = "covariance")
}else{
# cholesky is applied to the full matrix
if(any(str_detect(rec_flag_names, "cholesky"))){
mat_opts <- c("diag" = "cholesky", "off_diag" = "cholesky")
}else{
# Handle diagonal
if(any(str_detect(rec_flag_names, "standard"))){
mat_opts <- c("diag" = "standard")
}else{
mat_opts <- c("diag" = "variance")
}

# Handle off-diagonal
if(any(str_detect(rec_flag_names, "correlation"))){
mat_opts <- c(mat_opts, "off_diag" = "correlation")
}else{
mat_opts <- c(mat_opts, "off_diag" = "covariance")
}
}
}

tibble::tibble(
record_type = type, record_number = as.character(rec_num),
mat_class = mat_class, record_size = record_sizes[rec_num],
diag = mat_opts[["diag"]], off_diag = mat_opts[["off_diag"]]) %>%
dplyr::left_join(mat_specs[rec_num, ], by = c("record_number", "mat_class")) %>%
dplyr::relocate(c("diag", "off_diag"), .after = dplyr::everything())
})
}

bind_rows(
extract_mat_opts(ctl, "omega"),
extract_mat_opts(ctl, "sigma")
)
}


#' Get matrix type for omega and sigma records
#'
#' Returns a vector denoting either "block" or "diagonal" for each record.
#'
#' @param records Either a list of records, or a single `nmrec_record` object
#'
#' @return a vector
#' @noRd
get_matrix_types <- function(records){
if(!inherits(records, "list")) records <- list(records)

if(!length(records)){
return(
tibble::tibble(
record_number = NA, mat_class = NA,
subtype = NA, same = NA, same_n = NA
)
)
}

purrr::imap_dfr(records, function(rec, rec_num){
rec$parse()

# Matrix class
mat_class <- ifelse(
is.null(nmrec::get_record_option(rec, "block")),
"diagonal", "block"
)


# Parse `SAME` option if any
same_lbl <- nmrec::get_record_option(rec, "same")
same <- ifelse(is.null(same_lbl), FALSE, TRUE)
if(isTRUE(same)){
if(inherits(same_lbl, "nmrec_option_value")) {
same_n <- readr::parse_number(same_lbl$value)
if(is.na(same_n)) {
rlang::abort(c("Failed to parse same (n) value.", rec$format()))
}
}else{
same_n <- 1L
}
}else{
same_n <- NA
}

# Tabulate matrix subtypes
popts <- param_options(rec)
if(matrix_is_vpair(popts)){
subtype <- "vpair"
}else if(matrix_has_values(popts)){
subtype <- "values"
}else{
subtype <- "plain"
}

tibble::tibble(
record_number = as.character(rec_num), mat_class = mat_class,
subtype = subtype, same = same, same_n = same_n,
param_x = list(purrr::map_int(popts, param_x))
)
})
}


#' Get record number of initial estimates
#'
#' @param inits initial estimates object as returned by `nmrec::extract_*`
Expand Down Expand Up @@ -269,3 +489,53 @@ using_old_priors <- function(ctl) {

return(FALSE)
}



## The functions below were either taken directly from `nmrec`, or slightly
## adjusted (except `matrix_has_values`).

#' Return all options for initial estimates
#' @noRd
param_options <- function(record) {
name <- record[["name"]]
purrr::keep(record$get_options(), function(o) {
inherits(o, "nmrec_option_nested") && identical(o[["name"]], name)
})
}

#' Do the parameter options represent VALUES(diag,odiag) pair?
#' @param popts list of options for initial estimates. Output of `param_options()`.
#' @noRd
matrix_is_vpair <- function(popts) {
length(popts) == 1 &&
purrr::some(popts[[1]]$values, function(v) {
inherits(v, "nmrec_option") && v[["name"]] == "values"
})
}


#' Do the parameter options contain the form (0.01)x2 0.1?
#' @param popts list of options for initial estimates. Output of `param_options()`.
#' @noRd
matrix_has_values <- function(popts){
lengths <- purrr::map_int(popts, param_x)
return(any(lengths > 1))
}

#' Get number of inferred values per option.
#'
#' In cases where values are specified like `(0.01)x2 0.1`, we would extract `2`
#' since the value `0.01` is repeated twice.
#' @inheritParams matrix_has_values
#' @noRd
param_x <- function(popt) {
xopt <- purrr::keep(popt$values, function(x) {
inherits(x, "nmrec_option") && x[["name"]] == "x"
})
n_opts <- length(xopt)
if(!n_opts) return(1L)

x <- strtoi(xopt[[1]]$value, base = 10)
return(x)
}
Loading