|
| 1 | +#' Calculate CWRES (Conditional Weighted Residuals) |
| 2 | +#' |
| 3 | +#' Computes CWRES following the FOCE approximation method described in |
| 4 | +#' Hooker et al. (2007). The Jacobian of model predictions with respect to |
| 5 | +#' random effects (etas) is computed via central finite differences. |
| 6 | +#' |
| 7 | +#' CWRES = L^{-1} * (y - f(eta_hat) + F * eta_hat) |
| 8 | +#' |
| 9 | +#' where: |
| 10 | +#' - f(eta_hat) = individual predictions (IPRED) in the transformed space |
| 11 | +#' - F = Jacobian df/deta evaluated at eta_hat |
| 12 | +#' - L * L^T = V = F * Omega * F^T + Sigma (FOCE variance) |
| 13 | +#' - Sigma = diagonal residual error variance matrix |
| 14 | +#' |
| 15 | +#' @references Hooker AC, Staatz CE, Karlsson MO. Conditional weighted |
| 16 | +#' residuals (CWRES): a model diagnostic for the FOCE method. |
| 17 | +#' Pharm Res. 2007;24(12):2187-2197. |
| 18 | +#' |
| 19 | +#' @param eta_hat numeric vector of estimated etas (MAP estimates) |
| 20 | +#' @param ipred_raw numeric vector of raw (untransformed) individual |
| 21 | +#' predictions at eta_hat |
| 22 | +#' @param y numeric vector of observed values |
| 23 | +#' @param omega_full full omega covariance matrix |
| 24 | +#' @param error list with `prop` and `add` residual error components |
| 25 | +#' @param obs_type integer vector of observation types |
| 26 | +#' @param transf transformation function (identity or log for LTBS) |
| 27 | +#' @param model PKPDsim model object |
| 28 | +#' @param parameters_population list of population parameters |
| 29 | +#' @param nonfixed character vector of non-fixed parameter names |
| 30 | +#' @param as_eta character vector of parameters estimated directly as eta |
| 31 | +#' @param covariates list of PKPDsim covariates |
| 32 | +#' @param regimen PKPDsim regimen object |
| 33 | +#' @param lagtime lagtime specification |
| 34 | +#' @param t_obs numeric vector of observation times |
| 35 | +#' @param obs_type_sim observation type vector for simulation |
| 36 | +#' @param int_step_size integrator step size |
| 37 | +#' @param iov_bins IOV bin specification |
| 38 | +#' @param A_init initial state vector |
| 39 | +#' @param t_init initialization time |
| 40 | +#' @param steady_state_analytic steady state settings (or NULL) |
| 41 | +#' @param delta perturbation size for finite differences |
| 42 | +#' @param ... additional arguments passed to PKPDsim::sim_ode |
| 43 | +#' |
| 44 | +#' @return list with components: |
| 45 | +#' \item{cwres}{numeric vector of CWRES values} |
| 46 | +#' \item{vcov}{FOCE variance-covariance matrix of eta estimates |
| 47 | +#' (n_eta x n_eta), or NULL if computation failed. Derived from the |
| 48 | +#' same Jacobian F used for CWRES: vcov = (Omega^-1 + F' Sigma^-1 F)^-1} |
| 49 | +#' |
| 50 | +calc_cwres <- function( |
| 51 | + eta_hat, |
| 52 | + ipred_raw, |
| 53 | + y, |
| 54 | + omega_full, |
| 55 | + error, |
| 56 | + obs_type, |
| 57 | + transf, |
| 58 | + model, |
| 59 | + parameters_population, |
| 60 | + nonfixed, |
| 61 | + as_eta, |
| 62 | + covariates, |
| 63 | + regimen, |
| 64 | + lagtime, |
| 65 | + t_obs, |
| 66 | + obs_type_sim, |
| 67 | + int_step_size = 0.1, |
| 68 | + iov_bins = NULL, |
| 69 | + A_init = NULL, |
| 70 | + t_init = 0, |
| 71 | + steady_state_analytic = NULL, |
| 72 | + delta = 1e-4, |
| 73 | + ... |
| 74 | +) { |
| 75 | + n_obs <- length(y) |
| 76 | + n_eta <- length(eta_hat) |
| 77 | + |
| 78 | + if (n_obs == 0 || n_eta == 0) { |
| 79 | + return(list(cwres = numeric(0), vcov = NULL)) |
| 80 | + } |
| 81 | + |
| 82 | + # Transformed individual predictions at eta_hat |
| 83 | + ipred_transf <- transf(ipred_raw) |
| 84 | + |
| 85 | + # Compute Jacobian F = d(transf(f))/d(eta) via central finite differences |
| 86 | + F_matrix <- matrix(0, nrow = n_obs, ncol = n_eta) |
| 87 | + for (j in seq_len(n_eta)) { |
| 88 | + eta_plus <- eta_hat |
| 89 | + eta_minus <- eta_hat |
| 90 | + eta_plus[j] <- eta_hat[j] + delta |
| 91 | + eta_minus[j] <- eta_hat[j] - delta |
| 92 | + |
| 93 | + pred_plus <- simulate_with_etas( |
| 94 | + eta_plus, parameters_population, nonfixed, as_eta, |
| 95 | + model, covariates, regimen, lagtime, t_obs, obs_type_sim, |
| 96 | + int_step_size, iov_bins, A_init, t_init, |
| 97 | + steady_state_analytic, ... |
| 98 | + ) |
| 99 | + pred_minus <- simulate_with_etas( |
| 100 | + eta_minus, parameters_population, nonfixed, as_eta, |
| 101 | + model, covariates, regimen, lagtime, t_obs, obs_type_sim, |
| 102 | + int_step_size, iov_bins, A_init, t_init, |
| 103 | + steady_state_analytic, ... |
| 104 | + ) |
| 105 | + |
| 106 | + F_matrix[, j] <- (transf(pred_plus) - transf(pred_minus)) / (2 * delta) |
| 107 | + } |
| 108 | + |
| 109 | + # Residual error variance diagonal (in transformed space) |
| 110 | + sigma_diag <- error$prop[obs_type]^2 * ipred_transf^2 + |
| 111 | + error$add[obs_type]^2 |
| 112 | + |
| 113 | + # Omega submatrix for estimated parameters |
| 114 | + omega_est <- omega_full[seq_len(n_eta), seq_len(n_eta), drop = FALSE] |
| 115 | + |
| 116 | + # FOCE approximate marginal variance: V = F * Omega * F^T + Sigma |
| 117 | + V <- F_matrix %*% omega_est %*% t(F_matrix) + |
| 118 | + diag(sigma_diag, nrow = n_obs) |
| 119 | + |
| 120 | + # Cholesky decomposition (lower triangular) |
| 121 | + L <- tryCatch( |
| 122 | + t(chol(V)), |
| 123 | + error = function(e) NULL |
| 124 | + ) |
| 125 | + cwres <- if (is.null(L)) { |
| 126 | + warning("CWRES computation failed: variance matrix is not positive definite.") |
| 127 | + rep(NA_real_, n_obs) |
| 128 | + } else { |
| 129 | + # CWRES = L^{-1} * (y_transf - ipred_transf + F * eta_hat) |
| 130 | + # Derivation: |
| 131 | + # E_FOCE(y) = f(eta_hat) - F * eta_hat (since E[eta] = 0) |
| 132 | + # y - E_FOCE(y) = y - f(eta_hat) + F * eta_hat |
| 133 | + y_transf <- transf(y) |
| 134 | + as.numeric( |
| 135 | + solve(L, y_transf - ipred_transf + F_matrix %*% eta_hat) |
| 136 | + ) |
| 137 | + } |
| 138 | + |
| 139 | + # FOCE Hessian: H = Omega^{-1} + F' * Sigma^{-1} * F |
| 140 | + # vcov of etas = H^{-1} |
| 141 | + # This reuses the Jacobian F already computed for CWRES, so no extra |
| 142 | + # simulations are needed (replaces the numDeriv::hessian computation). |
| 143 | + omega_est_inv <- tryCatch(solve(omega_est), error = function(e) NULL) |
| 144 | + vcov <- NULL |
| 145 | + if (!is.null(omega_est_inv)) { |
| 146 | + H_foce <- omega_est_inv + |
| 147 | + t(F_matrix) %*% diag(1 / sigma_diag, nrow = n_obs) %*% F_matrix |
| 148 | + vcov <- tryCatch(solve(H_foce), error = function(e) { |
| 149 | + warning("FOCE variance-covariance computation failed.") |
| 150 | + NULL |
| 151 | + }) |
| 152 | + } |
| 153 | + |
| 154 | + list(cwres = cwres, vcov = vcov) |
| 155 | +} |
| 156 | + |
| 157 | + |
| 158 | +#' Simulate predictions with a given eta vector |
| 159 | +#' |
| 160 | +#' Helper for computing the Jacobian in CWRES calculation via finite |
| 161 | +#' differences. Computes individual parameters from etas and runs a |
| 162 | +#' PKPDsim simulation. |
| 163 | +#' |
| 164 | +#' @keywords internal |
| 165 | +simulate_with_etas <- function( |
| 166 | + eta, |
| 167 | + parameters_population, |
| 168 | + nonfixed, |
| 169 | + as_eta, |
| 170 | + model, |
| 171 | + covariates, |
| 172 | + regimen, |
| 173 | + lagtime, |
| 174 | + t_obs, |
| 175 | + obs_type, |
| 176 | + int_step_size, |
| 177 | + iov_bins, |
| 178 | + A_init, |
| 179 | + t_init, |
| 180 | + steady_state_analytic, |
| 181 | + ... |
| 182 | +) { |
| 183 | + # Compute individual parameters from etas |
| 184 | + par <- parameters_population |
| 185 | + for (i in seq_along(nonfixed)) { |
| 186 | + key <- nonfixed[i] |
| 187 | + if (key %in% as_eta) { |
| 188 | + par[[key]] <- eta[i] |
| 189 | + } else { |
| 190 | + par[[key]] <- par[[key]] * exp(eta[i]) |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + # Recompute steady-state A_init if needed |
| 195 | + a_init <- A_init |
| 196 | + if (!is.null(steady_state_analytic)) { |
| 197 | + a_init <- PKPDsim::calc_ss_analytic( |
| 198 | + f = steady_state_analytic$f, |
| 199 | + dose = regimen$dose_amts[1], |
| 200 | + interval = regimen$interval[1], |
| 201 | + model = model, |
| 202 | + parameters = par, |
| 203 | + covariates = covariates, |
| 204 | + map = steady_state_analytic$map, |
| 205 | + n_transit_compartments = PKPDsim::ifelse0( |
| 206 | + steady_state_analytic$n_transit_compartments, FALSE |
| 207 | + ), |
| 208 | + auc = PKPDsim::ifelse0(steady_state_analytic$auc, FALSE) |
| 209 | + ) |
| 210 | + } |
| 211 | + |
| 212 | + suppressMessages({ |
| 213 | + sim <- PKPDsim::sim_ode( |
| 214 | + ode = model, |
| 215 | + parameters = par, |
| 216 | + mixture_group = NULL, |
| 217 | + covariates = covariates, |
| 218 | + n_ind = 1, |
| 219 | + int_step_size = int_step_size, |
| 220 | + regimen = regimen, |
| 221 | + t_obs = t_obs, |
| 222 | + obs_type = obs_type, |
| 223 | + only_obs = TRUE, |
| 224 | + checks = FALSE, |
| 225 | + A_init = a_init, |
| 226 | + iov_bins = iov_bins, |
| 227 | + t_init = t_init, |
| 228 | + lagtime = lagtime, |
| 229 | + ... |
| 230 | + ) |
| 231 | + }) |
| 232 | + |
| 233 | + sim$y |
| 234 | +} |
0 commit comments