R/design-samplesize.R

Defines functions .r4vn_ss_formula_preview .r4vn_ss_sensitivity .r4vn_ss_get_method .r4vn_ss_registry_table .r4vn_ss_inputs_common .r4vn_ss_method_paragraph .r4vn_ss_calculate .r4vn_ss_calc_prediction .r4vn_ss_calc_statpsych .r4vn_ss_calc_trialsize .r4vn_ss_calc_webpower .r4vn_ss_calc_presize .r4vn_ss_calc_direct

# R4VN Study Design Studio -------------------------------------------------
# Sample-size registry and calculation engine.
# Public access is through design(); functions in this file are internal.

.r4vn_ss_calc_direct <- function(engine, v) {
  alpha <- v$alpha %||% 0.05
  power <- v$power %||% 0.80
  za2 <- .r4vn_ss_z(alpha, TRUE)
  za1 <- .r4vn_ss_z(alpha, FALSE)
  zb <- .r4vn_ss_zpower(power)

  if (engine == "prop_precision") {
    p <- v$p; d <- v$d
    .r4vn_ss_validate_prob(p, "Expected proportion")
    .r4vn_ss_validate_pos(d, "Absolute precision")
    n <- za2^2 * p * (1 - p) / d^2
    return(.r4vn_ss_result(
      n,
      formula = "n = \\frac{Z_{1-\\alpha/2}^2 p(1-p)}{d^2}",
      substitution = sprintf("n = \\frac{%.4f^2 \\times %.4f \\times (1-%.4f)}{%.4f^2}", za2, p, p, d),
      steps = c(sprintf("Unrounded n = %.4f", n), sprintf("Minimum required n = %d", ceiling(n)))
    ))
  }

  if (engine == "prop_relative_precision") {
    p <- v$p; rel <- v$relative_precision
    .r4vn_ss_validate_prob(p, "Expected proportion")
    .r4vn_ss_validate_pos(rel, "Relative precision")
    d <- rel * p
    n <- za2^2 * p * (1 - p) / d^2
    return(.r4vn_ss_result(
      n,
      formula = "d = rp, \\qquad n = \\frac{Z_{1-\\alpha/2}^2 p(1-p)}{d^2}",
      substitution = sprintf("d = %.4f \\times %.4f = %.4f; \\quad n = \\frac{%.4f^2 \\times %.4f \\times %.4f}{%.4f^2}", rel, p, d, za2, p, 1-p, d),
      steps = c(sprintf("Absolute precision = %.4f", d), sprintf("Unrounded n = %.4f", n), sprintf("Minimum required n = %d", ceiling(n)))
    ))
  }

  if (engine == "mean_precision") {
    sd <- v$sd; d <- v$d
    .r4vn_ss_validate_pos(sd, "Standard deviation")
    .r4vn_ss_validate_pos(d, "Absolute precision")
    n <- (za2 * sd / d)^2
    return(.r4vn_ss_result(
      n,
      formula = "n = \\left(\\frac{Z_{1-\\alpha/2}\\sigma}{d}\\right)^2",
      substitution = sprintf("n = \\left(\\frac{%.4f \\times %.4f}{%.4f}\\right)^2", za2, sd, d),
      steps = c(sprintf("Unrounded n = %.4f", n), sprintf("Minimum required n = %d", ceiling(n)))
    ))
  }

  if (engine == "sensitivity_precision" || engine == "specificity_precision") {
    acc <- if (engine == "sensitivity_precision") v$sensitivity else v$specificity
    prev <- v$prevalence; d <- v$d
    .r4vn_ss_validate_prob(acc, if (engine == "sensitivity_precision") "Sensitivity" else "Specificity")
    .r4vn_ss_validate_prob(prev, "Prevalence")
    .r4vn_ss_validate_pos(d, "Absolute precision")
    n_target <- za2^2 * acc * (1 - acc) / d^2
    denom <- if (engine == "sensitivity_precision") prev else 1 - prev
    n_total <- n_target / denom
    label <- if (engine == "sensitivity_precision") "disease-positive" else "disease-negative"
    return(.r4vn_ss_result(
      n_total,
      formula = "n_{target}=\\frac{Z_{1-\\alpha/2}^2 A(1-A)}{d^2}, \\qquad N=\\frac{n_{target}}{P(target)}",
      substitution = sprintf("n_{target}=\\frac{%.4f^2 \\times %.4f \\times %.4f}{%.4f^2}=%.3f; \\quad N=\\frac{%.3f}{%.4f}=%.3f", za2, acc, 1-acc, d, n_target, n_target, denom, n_total),
      steps = c(sprintf("Required %s participants = %d", label, ceiling(n_target)), sprintf("Required total participants = %d", ceiling(n_total))),
      details = list(target_n = ceiling(n_target), target_fraction = denom)
    ))
  }

  if (engine == "one_prop_test") {
    p0 <- v$p0; p1 <- v$p1
    .r4vn_ss_validate_prob(p0, "Null proportion")
    .r4vn_ss_validate_prob(p1, "Expected proportion")
    if (p0 == p1) stop("Null and expected proportions must differ.", call. = FALSE)
    n <- (za2 * sqrt(p0*(1-p0)) + zb * sqrt(p1*(1-p1)))^2 / (p1-p0)^2
    return(.r4vn_ss_result(
      n,
      formula = "n = \\frac{[Z_{1-\\alpha/2}\\sqrt{p_0(1-p_0)}+Z_{1-\\beta}\\sqrt{p_1(1-p_1)}]^2}{(p_1-p_0)^2}",
      substitution = sprintf("n=\\frac{[%.4f\\sqrt{%.4f(%.4f)}+%.4f\\sqrt{%.4f(%.4f)}]^2}{(%.4f-%.4f)^2}", za2,p0,1-p0,zb,p1,1-p1,p1,p0),
      steps = c(sprintf("Unrounded n = %.4f", n), sprintf("Minimum required n = %d", ceiling(n)))
    ))
  }

  if (engine == "two_prop") {
    p1 <- v$p1; p2 <- v$p2; r <- v$ratio %||% 1
    .r4vn_ss_validate_prob(p1, "Group 1 proportion")
    .r4vn_ss_validate_prob(p2, "Group 2 proportion")
    .r4vn_ss_validate_pos(r, "Allocation ratio n2/n1")
    if (p1 == p2) stop("The two expected proportions must differ.", call. = FALSE)
    pbar <- (p1 + r*p2)/(1+r)
    n1 <- (za2*sqrt((1+1/r)*pbar*(1-pbar)) + zb*sqrt(p1*(1-p1)+p2*(1-p2)/r))^2/(p1-p2)^2
    n2 <- r*n1
    return(.r4vn_ss_result(
      n1+n2, group_n = c(n1,n2),
      formula = "n_1=\\frac{[Z_{1-\\alpha/2}\\sqrt{(1+1/r)\\bar p(1-\\bar p)}+Z_{1-\\beta}\\sqrt{p_1(1-p_1)+p_2(1-p_2)/r}]^2}{(p_1-p_2)^2},\\quad n_2=rn_1",
      substitution = sprintf("\\bar p=%.4f;\\quad n_1=%.4f;\\quad n_2=%.4f", pbar,n1,n2),
      steps = c(sprintf("Group 1 unrounded n = %.4f",n1),sprintf("Group 2 unrounded n = %.4f",n2),sprintf("Base total after rounding = %d",ceiling(n1)+ceiling(n2))),
      details = list(pbar=pbar, ratio=r)
    ))
  }

  if (engine == "paired_prop") {
    p01 <- v$p01; p10 <- v$p10
    if (!is.finite(p01) || !is.finite(p10) || p01 < 0 || p10 < 0 || p01+p10 > 1) stop("Discordant proportions must be non-negative and sum to <= 1.", call. = FALSE)
    delta <- abs(p01-p10)
    if (delta == 0) stop("Discordant proportions must differ.", call. = FALSE)
    q <- p01+p10
    n <- (za2*sqrt(q) + zb*sqrt(q-delta^2))^2/delta^2
    return(.r4vn_ss_result(
      n,
      formula = "n = \\frac{[Z_{1-\\alpha/2}\\sqrt{p_{01}+p_{10}}+Z_{1-\\beta}\\sqrt{p_{01}+p_{10}-(p_{01}-p_{10})^2}]^2}{(p_{01}-p_{10})^2}",
      substitution = sprintf("n=\\frac{[%.4f\\sqrt{%.4f+%.4f}+%.4f\\sqrt{%.4f+%.4f-(%.4f-%.4f)^2}]^2}{(%.4f-%.4f)^2}",za2,p01,p10,zb,p01,p10,p01,p10,p01,p10),
      steps = c(sprintf("Unrounded n = %.4f",n),sprintf("Minimum paired sample = %d",ceiling(n)))
    ))
  }

  if (engine == "one_mean_test") {
    sd <- v$sd; delta <- abs(v$delta)
    .r4vn_ss_validate_pos(sd, "Standard deviation"); .r4vn_ss_validate_pos(delta,"Mean difference")
    n <- ((za2+zb)*sd/delta)^2
    return(.r4vn_ss_result(n,
      formula="n=\\left(\\frac{(Z_{1-\\alpha/2}+Z_{1-\\beta})\\sigma}{\\Delta}\\right)^2",
      substitution=sprintf("n=\\left(\\frac{(%.4f+%.4f)\\times%.4f}{%.4f}\\right)^2",za2,zb,sd,delta),
      steps=c(sprintf("Unrounded n = %.4f",n),sprintf("Minimum required n = %d",ceiling(n)))))
  }

  if (engine == "two_mean") {
    sd <- v$sd; delta <- abs(v$delta); r <- v$ratio %||% 1
    .r4vn_ss_validate_pos(sd,"Pooled standard deviation"); .r4vn_ss_validate_pos(delta,"Mean difference"); .r4vn_ss_validate_pos(r,"Allocation ratio n2/n1")
    n1 <- (za2+zb)^2*sd^2*(1+1/r)/delta^2
    n2 <- r*n1
    return(.r4vn_ss_result(n1+n2, group_n=c(n1,n2),
      formula="n_1=\\frac{(Z_{1-\\alpha/2}+Z_{1-\\beta})^2\\sigma^2(1+1/r)}{\\Delta^2},\\quad n_2=rn_1",
      substitution=sprintf("n_1=\\frac{(%.4f+%.4f)^2\\times%.4f^2\\times(1+1/%.4f)}{%.4f^2}=%.4f;\\quad n_2=%.4f",za2,zb,sd,r,delta,n1,n2),
      steps=c(sprintf("Group 1 = %d",ceiling(n1)),sprintf("Group 2 = %d",ceiling(n2)),sprintf("Total = %d",ceiling(n1)+ceiling(n2)))))
  }

  if (engine == "paired_mean") {
    sd <- v$sd_diff; delta <- abs(v$delta)
    .r4vn_ss_validate_pos(sd,"SD of paired differences"); .r4vn_ss_validate_pos(delta,"Mean paired difference")
    n <- ((za2+zb)*sd/delta)^2
    return(.r4vn_ss_result(n,
      formula="n=\\left(\\frac{(Z_{1-\\alpha/2}+Z_{1-\\beta})\\sigma_D}{\\Delta}\\right)^2",
      substitution=sprintf("n=\\left(\\frac{(%.4f+%.4f)\\times%.4f}{%.4f}\\right)^2",za2,zb,sd,delta),
      steps=c(sprintf("Unrounded n = %.4f",n),sprintf("Minimum paired sample = %d",ceiling(n)))))
  }

  if (engine == "correlation_test") {
    r0 <- v$r0; r1 <- v$r1
    if (!is.finite(r0)||!is.finite(r1)||abs(r0)>=1||abs(r1)>=1) stop("Correlations must be between -1 and 1.",call.=FALSE)
    dz <- abs(atanh(r1)-atanh(r0)); if (dz==0) stop("Expected and null correlations must differ.",call.=FALSE)
    n <- 3 + ((za2+zb)/dz)^2
    return(.r4vn_ss_result(n,
      formula="n=3+\\left[\\frac{Z_{1-\\alpha/2}+Z_{1-\\beta}}{\\operatorname{atanh}(r_1)-\\operatorname{atanh}(r_0)}\\right]^2",
      substitution=sprintf("n=3+[(%.4f+%.4f)/(atanh(%.4f)-atanh(%.4f))]^2",za2,zb,r1,r0),
      steps=c(sprintf("Fisher-z difference = %.4f",dz),sprintf("Unrounded n = %.4f",n),sprintf("Minimum required n = %d",ceiling(n)))))
  }

  if (engine == "two_correlations") {
    r1 <- v$r1; r2 <- v$r2
    if (abs(r1)>=1||abs(r2)>=1) stop("Correlations must be between -1 and 1.",call.=FALSE)
    dz <- abs(atanh(r1)-atanh(r2)); if (dz==0) stop("Correlations must differ.",call.=FALSE)
    n <- 3 + 2*((za2+zb)/dz)^2
    return(.r4vn_ss_result(2*n, group_n=c(n,n),
      formula="n_{per\\ group}=3+2\\left[\\frac{Z_{1-\\alpha/2}+Z_{1-\\beta}}{z(r_1)-z(r_2)}\\right]^2",
      substitution=sprintf("n=3+2[(%.4f+%.4f)/(atanh(%.4f)-atanh(%.4f))]^2",za2,zb,r1,r2),
      steps=c(sprintf("Per-group unrounded n = %.4f",n),sprintf("Per group = %d",ceiling(n)),sprintf("Total = %d",2*ceiling(n)))))
  }

  if (engine == "two_rates") {
    l1 <- v$rate1; l2 <- v$rate2; r <- v$ratio %||% 1
    .r4vn_ss_validate_pos(l1,"Rate 1"); .r4vn_ss_validate_pos(l2,"Rate 2"); .r4vn_ss_validate_pos(r,"Allocation ratio")
    d <- abs(l1-l2); if(d==0) stop("Rates must differ.",call.=FALSE)
    # Exposure-time units per group under normal approximation to independent Poisson rates.
    t1 <- (za2+zb)^2*(l1+l2/r)/d^2
    t2 <- r*t1
    return(.r4vn_ss_result(t1+t2, group_n=c(t1,t2),
      formula="T_1\\approx\\frac{(Z_{1-\\alpha/2}+Z_{1-\\beta})^2(\\lambda_1+\\lambda_2/r)}{(\\lambda_1-\\lambda_2)^2},\\quad T_2=rT_1",
      substitution=sprintf("T_1=((%.4f+%.4f)^2(%.4f+%.4f/%.4f))/(%.4f-%.4f)^2=%.4f",za2,zb,l1,l2,r,l1,l2,t1),
      steps=c(sprintf("Required exposure-time units group 1 = %.2f",t1),sprintf("Group 2 = %.2f",t2)),
      details=list(unit="exposure-time units")))
  }

  if (engine == "logrank_hr") {
    hr <- v$hr; alloc <- v$allocation %||% 0.5; event_fraction <- v$event_fraction
    .r4vn_ss_validate_pos(hr,"Hazard ratio"); .r4vn_ss_validate_prob(alloc,"Allocation fraction to group 1"); .r4vn_ss_validate_prob(event_fraction,"Expected event fraction")
    if (hr==1) stop("Hazard ratio must differ from 1.",call.=FALSE)
    events <- (za2+zb)^2/(alloc*(1-alloc)*(log(hr)^2))
    total <- events/event_fraction
    n1 <- total*alloc; n2 <- total*(1-alloc)
    return(.r4vn_ss_result(total,group_n=c(n1,n2),
      formula="D=\\frac{(Z_{1-\\alpha/2}+Z_{1-\\beta})^2}{\\pi(1-\\pi)[\\log(HR)]^2},\\qquad N=\\frac{D}{P(event)}",
      substitution=sprintf("D=\\frac{(%.4f+%.4f)^2}{%.4f(%.4f)[log(%.4f)]^2}=%.3f;\\quad N=%.3f/%.4f=%.3f",za2,zb,alloc,1-alloc,hr,events,events,event_fraction,total),
      steps=c(sprintf("Required events = %d",ceiling(events)),sprintf("Required total participants = %d",ceiling(total))),details=list(events=events)))
  }

  if (engine == "cluster_deff") {
    base <- v$base_n; m <- v$cluster_size; icc <- v$icc
    .r4vn_ss_validate_pos(base,"Base SRS sample"); .r4vn_ss_validate_pos(m,"Average cluster size"); if(!is.finite(icc)||icc<0||icc>=1) stop("ICC must be >=0 and <1.",call.=FALSE)
    deff <- 1+(m-1)*icc; n <- base*deff
    return(.r4vn_ss_result(n,
      formula="DEFF=1+(m-1)\\rho,\\qquad n_c=n_{SRS}\\times DEFF",
      substitution=sprintf("DEFF=1+(%.4f-1)\\times%.4f=%.4f;\\quad n_c=%.4f\\times%.4f=%.4f",m,icc,deff,base,deff,n),
      steps=c(sprintf("Design effect = %.4f",deff),sprintf("Cluster-adjusted n = %d",ceiling(n))),details=list(deff=deff)))
  }

  if (engine == "efa_guidance") {
    items <- v$items; ratio <- v$subjects_per_item; min_n <- v$minimum_n
    .r4vn_ss_validate_pos(items,"Number of items"); .r4vn_ss_validate_pos(ratio,"Subjects per item"); .r4vn_ss_validate_pos(min_n,"Minimum sample")
    n <- max(items*ratio,min_n)
    return(.r4vn_ss_result(n,
      formula="N=\\max(k\\times r, N_{minimum})",
      substitution=sprintf("N=max(%d\\times%.2f,%d)=%.2f",as.integer(items),ratio,as.integer(min_n),n),
      steps=c(sprintf("Rule-based planning value = %d",ceiling(n)),"This is a planning guideline, not a universal factor-recovery formula."),
      details=list(rule_of_thumb=TRUE)))
  }

  if (engine == "ni_mean") {
    sd <- v$sd; distance <- v$distance_to_margin; r <- v$ratio %||% 1
    .r4vn_ss_validate_pos(sd,"Pooled SD"); .r4vn_ss_validate_pos(distance,"Distance from expected effect to NI margin"); .r4vn_ss_validate_pos(r,"Allocation ratio")
    n1 <- (za1+zb)^2*sd^2*(1+1/r)/distance^2; n2<-r*n1
    return(.r4vn_ss_result(n1+n2,group_n=c(n1,n2),
      formula="n_1=\\frac{(Z_{1-\\alpha}+Z_{1-\\beta})^2\\sigma^2(1+1/r)}{(\\Delta-\\Delta_{NI})^2},\\quad n_2=rn_1",
      substitution=sprintf("n_1=((%.4f+%.4f)^2\\times%.4f^2\\times(1+1/%.4f))/%.4f^2=%.4f",za1,zb,sd,r,distance,n1),
      steps=c(sprintf("Group 1 = %d",ceiling(n1)),sprintf("Group 2 = %d",ceiling(n2)),sprintf("Total = %d",ceiling(n1)+ceiling(n2)))))
  }

  if (engine == "crossover_mean") {
    sd <- v$sd_diff; delta <- abs(v$delta)
    .r4vn_ss_validate_pos(sd,"Within-subject SD of treatment difference"); .r4vn_ss_validate_pos(delta,"Mean treatment difference")
    n <- ((za2+zb)*sd/delta)^2
    return(.r4vn_ss_result(n,
      formula="n=\\left[\\frac{(Z_{1-\\alpha/2}+Z_{1-\\beta})\\sigma_D}{\\Delta}\\right]^2",
      substitution=sprintf("n=[((%.4f+%.4f)\\times%.4f)/%.4f]^2",za2,zb,sd,delta),
      steps=c(sprintf("Total evaluable participants = %d",ceiling(n)))))
  }

  stop(sprintf("Unknown direct sample-size engine '%s'.", engine), call. = FALSE)
}

.r4vn_ss_calc_presize <- function(engine, v) {
  .r4vn_design_require("presize", "precision-based sample-size calculation")
  x <- switch(engine,
    auc_precision = presize::prec_auc(auc=v$auc, prev=v$prevalence, conf.width=v$conf_width, conf.level=1-v$alpha),
    cor_precision_pearson = presize::prec_cor(r=v$r, conf.width=v$conf_width, conf.level=1-v$alpha, method="pearson"),
    cor_precision_spearman = presize::prec_cor(r=v$r, conf.width=v$conf_width, conf.level=1-v$alpha, method="spearman"),
    cor_precision_kendall = presize::prec_cor(r=v$r, conf.width=v$conf_width, conf.level=1-v$alpha, method="kendall"),
    cronbach_precision = presize::prec_cronb(k=as.integer(v$items), calpha=v$calpha, conf.level=1-v$alpha, conf.width=v$conf_width),
    icc_precision = presize::prec_icc(rho=v$icc, k=as.integer(v$raters), conf.width=v$conf_width, conf.level=1-v$alpha),
    kappa_precision = presize::prec_kappa(kappa=v$kappa, raters=as.integer(v$raters), n_category=2, props=c(v$prop_cat1,1-v$prop_cat1), conf.width=v$conf_width, conf.level=1-v$alpha),
    bland_altman_precision = presize::prec_lim_agree(conf.width=v$conf_width, conf.level=1-v$alpha),
    pos_lr_precision = presize::prec_pos_lr(prev=v$prevalence,sens=v$sensitivity,spec=v$specificity,conf.width=v$conf_width,conf.level=1-v$alpha),
    neg_lr_precision = presize::prec_neg_lr(prev=v$prevalence,sens=v$sensitivity,spec=v$specificity,conf.width=v$conf_width,conf.level=1-v$alpha),
    stop("Unknown presize engine.",call.=FALSE)
  )
  n <- .r4vn_ss_extract_scalar(x)
  if(!is.finite(n)) stop("Could not extract the required sample size from presize output.",call.=FALSE)
  subs <- paste(sprintf("%s=%s",names(v),vapply(v,function(z) paste(z,collapse=","),character(1))),collapse="; ")
  .r4vn_ss_result(n,
    formula="N = \\operatorname*{argmin}_N |W_{CI}(N;\\theta)-W_{target}|",
    substitution=.r4vn_design_html_escape(subs),
    steps=c(sprintf("Required sample size from presize = %.3f",n),sprintf("Minimum required n = %d",ceiling(n))),raw=x)
}

.r4vn_ss_calc_webpower <- function(engine, v) {
  .r4vn_design_require("WebPower", "advanced power/sample-size calculation")
  x <- switch(engine,
    wp_anova = WebPower::wp.anova(k=as.integer(v$groups),n=NULL,f=v$f,alpha=v$alpha,power=v$power,type="overall"),
    wp_anova_binary = WebPower::wp.anova.binary(k=as.integer(v$groups),n=NULL,V=v$effect,alpha=v$alpha,power=v$power),
    wp_anova_count = WebPower::wp.anova.count(k=as.integer(v$groups),n=NULL,V=v$effect,alpha=v$alpha,power=v$power),
    wp_regression = WebPower::wp.regression(n=NULL,p1=as.integer(v$tested_predictors),p2=as.integer(v$control_predictors),f2=v$f2,alpha=v$alpha,power=v$power),
    wp_logistic = WebPower::wp.logistic(n=NULL,p0=v$p0,p1=v$p1,alpha=v$alpha,power=v$power,family=v$predictor_family,parameter=if(v$predictor_family=="Bernoulli") v$predictor_parameter else c(0,1)),
    wp_poisson = WebPower::wp.poisson(n=NULL,exp0=v$base_rate,exp1=v$rate_ratio,alpha=v$alpha,power=v$power,family="Bernoulli",parameter=v$predictor_parameter),
    wp_rmanova = WebPower::wp.rmanova(n=NULL,ng=as.integer(v$groups),nm=as.integer(v$measurements),f=v$f,nscor=v$nonsphericity,alpha=v$alpha,power=v$power,type=0),
    wp_sem_rmsea = WebPower::wp.sem.rmsea(n=NULL,df=as.integer(v$df),rmsea0=v$rmsea0,rmsea1=v$rmsea1,power=v$power,alpha=v$alpha,type=v$fit_test),
    wp_mediation = WebPower::wp.mediation(n=NULL,power=v$power,a=v$a,b=v$b,varx=v$varx,vary=v$vary,varm=v$varm,alpha=v$alpha),
    wp_crt2arm = WebPower::wp.crt2arm(n=NULL,f=v$f,J=as.integer(v$clusters_per_arm),icc=v$icc,power=v$power,alpha=v$alpha,alternative="two.sided"),
    stop("Unknown WebPower engine.",call.=FALSE)
  )
  n <- .r4vn_ss_extract_scalar(x)
  if(!is.finite(n)) stop("Could not extract sample size from WebPower output.",call.=FALSE)
  if(engine=="wp_crt2arm") {
    total <- n * as.integer(v$clusters_per_arm) * 2
    return(.r4vn_ss_result(total,
      formula="Power equation for a two-arm cluster randomized trial (WebPower)",
      substitution=paste(sprintf("%s=%s",names(v),v),collapse="; "),
      steps=c(sprintf("Required subjects per cluster = %.3f",n),sprintf("Clusters per arm = %d",as.integer(v$clusters_per_arm)),sprintf("Approximate total participants = %d",ceiling(total))),
      details=list(subjects_per_cluster=n,clusters_per_arm=as.integer(v$clusters_per_arm)),raw=x))
  }
  .r4vn_ss_result(n,
    formula="N is obtained by numerical inversion of the method-specific power equation.",
    substitution=paste(sprintf("%s=%s",names(v),v),collapse="; "),
    steps=c(sprintf("Required sample size from WebPower = %.3f",n),sprintf("Minimum required n = %d",ceiling(n))),raw=x)
}

.r4vn_ss_calc_trialsize <- function(engine, v) {
  .r4vn_design_require("TrialSize", "clinical-trial sample-size calculation")
  beta <- 1-v$power
  fn <- getExportedValue("TrialSize", engine)
  args <- switch(engine,
    "OneSampleMean.Equality"=list(alpha=v$alpha,beta=beta,sigma=v$sd,margin=v$delta),
    "OneSampleMean.Equivalence"=list(alpha=v$alpha,beta=beta,sigma=v$sd,margin=v$expected_diff,delta=v$margin),
    "OneSampleMean.NIS"=list(alpha=v$alpha,beta=beta,sigma=v$sd,margin=v$expected_diff,delta=v$margin),
    "TwoSampleMean.Equality"=list(alpha=v$alpha,beta=beta,sigma=v$sd,k=1/v$ratio,margin=v$delta),
    "TwoSampleMean.Equivalence"=list(alpha=v$alpha,beta=beta,sigma=v$sd,k=1/v$ratio,delta=v$margin,margin=v$expected_diff),
    "TwoSampleMean.NIS"=list(alpha=v$alpha,beta=beta,sigma=v$sd,k=1/v$ratio,delta=v$margin,margin=v$expected_diff),
    "TwoSampleProportion.Equality"=list(alpha=v$alpha,beta=beta,p1=v$p1,p2=v$p2,k=1/v$ratio),
    "TwoSampleProportion.Equivalence"=list(alpha=v$alpha,beta=beta,p1=v$p1,p2=v$p2,k=1/v$ratio,delta=v$expected_diff,margin=v$margin),
    "TwoSampleProportion.NIS"=list(alpha=v$alpha,beta=beta,p1=v$p1,p2=v$p2,k=1/v$ratio,delta=v$expected_diff,margin=v$margin),
    "TwoSampleCrossOver.Equality"=list(alpha=v$alpha,beta=beta,sigma=v$sd_diff,margin=v$delta),
    "TwoSampleCrossOver.Equivalence"=list(alpha=v$alpha,beta=beta,sigma=v$sd_diff,delta=v$margin,margin=v$expected_diff),
    "TwoSampleCrossOver.NIS"=list(alpha=v$alpha,beta=beta,sigma=v$sd_diff,delta=v$margin,margin=v$expected_diff),
    stop("TrialSize wrapper not configured for this method.",call.=FALSE)
  )
  x <- do.call(fn,args)
  n <- .r4vn_ss_extract_scalar(x)
  if(!is.finite(n) && is.numeric(x)) n <- max(as.numeric(x),na.rm=TRUE)
  if(!is.finite(n)) stop("Could not extract sample size from TrialSize output.",call.=FALSE)
  # TrialSize two-independent-group functions return the group-1 sample size;
  # k=n1/n2. R4VN exposes ratio as n2/n1, so derive group 2 explicitly.
  is_two_group <- engine %in% c("TwoSampleMean.Equality","TwoSampleMean.Equivalence","TwoSampleMean.NIS",
                                "TwoSampleProportion.Equality","TwoSampleProportion.Equivalence","TwoSampleProportion.NIS")
  group_n <- if(is_two_group) c(n, n * v$ratio) else n
  .r4vn_ss_result(sum(group_n), group_n=group_n,
    formula=paste0("Sample-size equation implemented by TrialSize::",engine," (Chow, Shao & Wang method)."),
    substitution=paste(sprintf("%s=%s",names(args),vapply(args,as.character,character(1))),collapse="; "),
    steps=if(is_two_group) c(sprintf("Group 1 sample size = %.3f",group_n[1]),sprintf("Group 2 sample size = %.3f",group_n[2]),sprintf("Rounded total = %d",sum(ceiling(group_n)))) else c(sprintf("Required sample size reported by TrialSize = %.3f",n),sprintf("Rounded target = %d",ceiling(n))),raw=x)
}

.r4vn_ss_calc_statpsych <- function(engine, v) {
  .r4vn_design_require("statpsych", "reliability and precision sample-size calculation")
  args <- switch(engine,
    stat_cronbach_test = list(alpha=v$alpha, pow=v$power, rel=v$expected_alpha, r=as.integer(v$items), h=v$null_alpha),
    stat_cronbach2_test = list(alpha=v$alpha, pow=v$power, rel1=v$alpha1, rel2=v$alpha2, r=as.integer(v$items)),
    stat_cronbach2_ci = list(alpha=v$alpha, rel1=v$alpha1, rel2=v$alpha2, r=as.integer(v$items), w=v$conf_width),
    stat_cv_ci = list(alpha=v$alpha, CV=v$cv, w=v$conf_width),
    stat_etasqr_ci = list(alpha=v$alpha, etasqr=v$eta2, groups=as.integer(v$groups), w=v$conf_width),
    stat_cor2_ci = list(alpha=v$alpha, cor1=v$r1, cor2=v$r2, w=v$conf_width),
    stat_mean2_ci = list(alpha=v$alpha, var=v$variance, w=v$conf_width, R=v$ratio),
    stop("Unknown statpsych engine.", call.=FALSE)
  )
  fname <- switch(engine,
    stat_cronbach_test="size.test.cronbach",
    stat_cronbach2_test="size.test.cronbach2",
    stat_cronbach2_ci="size.ci.cronbach2",
    stat_cv_ci="size.ci.cv",
    stat_etasqr_ci="size.ci.etasqr",
    stat_cor2_ci="size.ci.cor2",
    stat_mean2_ci="size.ci.mean2"
  )
  fn <- getExportedValue("statpsych", fname)
  x <- do.call(fn, args)
  vals <- as.numeric(unlist(x, recursive=TRUE, use.names=FALSE))
  vals <- vals[is.finite(vals) & vals > 0]
  if(!length(vals)) stop("Could not extract sample size from statpsych output.", call.=FALSE)
  # Functions that return n1,n2 are handled as group sizes; single-value functions return one n.
  if(engine %in% c("stat_cronbach2_test","stat_cronbach2_ci","stat_mean2_ci") && length(vals) >= 2) group_n <- vals[seq_len(2)] else if(engine %in% c("stat_cronbach2_test","stat_cronbach2_ci") && length(vals)==1) group_n <- c(vals[1],vals[1]) else group_n <- vals[1]
  .r4vn_ss_result(group_n,
    formula=paste0("Sample-size equation implemented by statpsych::",fname,"."),
    substitution=paste(sprintf("%s=%s", names(args), vapply(args, as.character, character(1))), collapse="; "),
    steps=c(sprintf("Required sample size%s = %s", if(length(group_n)>1) " by group" else "", paste(round(group_n,3), collapse=" + ")), sprintf("Rounded target = %d", sum(ceiling(group_n)))),
    raw=x)
}

.r4vn_ss_calc_prediction <- function(engine,v) {
  .r4vn_design_require("pmsampsize","prediction-model development sample-size calculation")
  x <- switch(engine,
    pms_binary = pmsampsize::pmsampsize(type="b",csrsquared=v$rsquared,parameters=as.integer(v$parameters),shrinkage=v$shrinkage,prevalence=v$prevalence),
    pms_continuous = pmsampsize::pmsampsize(type="c",rsquared=v$rsquared,parameters=as.integer(v$parameters),shrinkage=v$shrinkage,intercept=v$mean_outcome,sd=v$sd),
    pms_survival = pmsampsize::pmsampsize(type="s",csrsquared=v$rsquared,parameters=as.integer(v$parameters),shrinkage=v$shrinkage,rate=v$event_rate,timepoint=v$timepoint,meanfup=v$mean_followup),
    stop("Unknown pmsampsize engine.",call.=FALSE)
  )
  n <- .r4vn_ss_extract_scalar(x)
  if(!is.finite(n)) {
    # Last-resort: use the largest finite number in fields whose names suggest sample size.
    candidates <- numeric()
    recurse <- function(z,nm="") {
      if(is.numeric(z) && length(z) && grepl("sample|size|final|minimum|^n$",nm,ignore.case=TRUE)) candidates <<- c(candidates,as.numeric(z))
      if(is.list(z)) for(k in names(z)) recurse(z[[k]],k)
    }
    recurse(x)
    candidates <- candidates[is.finite(candidates) & candidates>1]
    if(length(candidates)) n <- max(candidates)
  }
  if(!is.finite(n)) stop("Could not identify the final recommended sample size in pmsampsize output.",call.=FALSE)
  .r4vn_ss_result(n,
    formula="N = max(N_1, N_2, ...), where each N_j satisfies a Riley et al. prediction-model sample-size criterion.",
    substitution=paste(sprintf("%s=%s",names(v),v),collapse="; "),
    steps=c(sprintf("Recommended sample size = %.3f",n),sprintf("Final rounded target = %d",ceiling(n))),raw=x)
}

.r4vn_ss_calculate <- function(method,v,loss=0,design_effect=1,population=NA_real_) {
  e <- method$engine
  res <- if(e %in% c("prop_precision","prop_relative_precision","mean_precision","sensitivity_precision","specificity_precision","one_prop_test","two_prop","paired_prop","one_mean_test","two_mean","paired_mean","correlation_test","two_correlations","two_rates","logrank_hr","cluster_deff","efa_guidance","ni_mean","crossover_mean")) {
    .r4vn_ss_calc_direct(e,v)
  } else if(startsWith(e,"wp_")) {
    .r4vn_ss_calc_webpower(e,v)
  } else if(startsWith(e,"pms_")) {
    .r4vn_ss_calc_prediction(e,v)
  } else if(startsWith(e,"stat_")) {
    .r4vn_ss_calc_statpsych(e,v)
  } else if(e %in% c("auc_precision","cor_precision_pearson","cor_precision_spearman","cor_precision_kendall","cronbach_precision","icc_precision","kappa_precision","bland_altman_precision","pos_lr_precision","neg_lr_precision")) {
    .r4vn_ss_calc_presize(e,v)
  } else if(startsWith(e,"TrialSize::")) {
    .r4vn_ss_calc_trialsize(sub("^TrialSize::","",e),v)
  } else stop("Sample-size engine is not implemented.",call.=FALSE)

  res$method_id <- method$id
  res$method_name <- method$name
  res$category <- method$category
  res$method_type <- method$method_type
  res$reference <- method$reference
  res$notes <- method$notes
  res$inputs <- v
  res$calculation_id <- .r4vn_design_id("SS")
  res$created <- as.character(Sys.time())
  res$r_version <- R.version.string
  res$r4vn_version <- .r4vn_design_r4vn_version()
  .r4vn_ss_adjust(res,loss=loss,design_effect=design_effect,population=population)
}

.r4vn_ss_method_paragraph <- function(x) {
  if(is.null(x)) return("")
  m <- tryCatch(.r4vn_ss_get_method(x$method_id), error=function(e) NULL)
  label_map <- if(is.null(m)) setNames(names(x$inputs),names(x$inputs)) else setNames(vapply(m$inputs,`[[`,character(1),"label"),vapply(m$inputs,`[[`,character(1),"id"))

  pct_ids <- c("p","p0","p1","p2","p01","p10","prevalence","sensitivity","specificity",
               "power","calpha","expected_alpha","null_alpha","alpha1","alpha2","event_fraction")
  vals <- vapply(names(x$inputs), function(nm) {
    z <- x$inputs[[nm]]
    if(is.numeric(z) && length(z)==1) {
      if(nm %in% pct_ids && is.finite(z) && z >= 0 && z <= 1) {
        paste0(formatC(100*z,digits=1,format="f"), "% (", formatC(z,digits=4,format="fg"), ")")
      } else formatC(z,digits=4,format="fg")
    } else paste(z,collapse=", ")
  }, character(1))
  labels <- unname(label_map[names(vals)]); labels[is.na(labels)] <- names(vals)[is.na(labels)]
  assumptions <- paste(paste0(labels," = ",vals),collapse="; ")
  base_total <- sum(ceiling(x$base_group_n_unrounded %||% x$group_n))
  adjustment <- character()
  if(isTRUE(x$fpc_applied)) adjustment <- c(adjustment,paste0("a finite population correction for N = ",format(x$population,scientific=FALSE)))
  if(!is.null(x$design_effect) && x$design_effect!=1) adjustment <- c(adjustment,paste0("a design effect of ",.r4vn_design_fmt(x$design_effect,3)))
  if(!is.null(x$loss) && x$loss>0) adjustment <- c(adjustment,paste0(.r4vn_design_pct(x$loss,1)," anticipated loss/non-response"))
  adjtxt <- if(length(adjustment)) paste0(" After accounting for ",paste(adjustment,collapse=", "),",") else ""
  paste0("The required sample size was calculated using the ",x$method_name," method. The prespecified assumptions were: ",assumptions,". The base required sample size was ",base_total,".",adjtxt," the final target sample size was ",x$final_n," participants.")
}

.r4vn_ss_inputs_common <- function() {
  list(
    alpha=.r4vn_ss_input("alpha","Significance level (alpha)",0.05,"Probability of a Type I error. For a two-sided 95% confidence level, alpha=0.05.",min=0.0001,max=0.5,step=0.01),
    power=.r4vn_ss_input("power","Power",0.80,"Probability of detecting the prespecified effect when it truly exists.",min=0.50,max=0.999,step=0.01),
    ratio=.r4vn_ss_input("ratio","Allocation ratio n2/n1",1,"Ratio of the group-2 sample size to the group-1 sample size.",min=0.01,max=100,step=0.1)
  )
}

.r4vn_ss_registry <- local({
  cache <- NULL
  function() {
    if(!is.null(cache)) return(cache)
    C <- .r4vn_ss_inputs_common()
    I <- .r4vn_ss_input
    M <- .r4vn_ss_method
    ref_lw <- "Lwanga SK, Lemeshow S. Sample Size Determination in Health Studies. WHO; 1991."
    ref_chow <- "Chow SC, Shao J, Wang H. Sample Size Calculations in Clinical Research."
    ref_web <- "Zhang Z, Yuan KH. Practical Statistical Power Analysis Using Webpower and R."
    ref_bonett <- "Bonett DG, Wright TA. Cronbach's alpha reliability: interval estimation, hypothesis testing, and sample size planning. J Organ Behav."

    cache <<- list(
      M("prop_abs","Estimation & precision","Estimate one proportion / prevalence","Estimate a population proportion with absolute precision.","prop_precision",
        list(I("p","Expected proportion",0.50,"Anticipated population proportion.",min=0.0001,max=0.9999,step=0.01),I("d","Absolute precision",0.05,"Desired half-width / margin of error on the proportion scale.",min=0.0001,max=0.5,step=0.005),C$alpha),
        "n = Z^2 p(1-p)/d^2",ref_lw,c("p","d","alpha")),
      M("prop_rel","Estimation & precision","Estimate one proportion with relative precision","Use a margin of error defined relative to the expected proportion.","prop_relative_precision",
        list(I("p","Expected proportion",0.20,"Anticipated population proportion.",min=0.0001,max=0.9999,step=0.01),I("relative_precision","Relative precision",0.20,"Absolute margin of error divided by p.",min=0.001,max=2,step=0.05),C$alpha),
        "d=rp; n=Z^2p(1-p)/d^2",ref_lw,c("p","relative_precision")),
      M("mean_precision","Estimation & precision","Estimate one mean","Estimate a population mean with a desired absolute precision.","mean_precision",
        list(I("sd","Expected standard deviation",10,"Expected population SD.",min=0.0001,max=1e9),I("d","Absolute precision",2,"Desired margin of error in outcome units.",min=0.0001,max=1e9),C$alpha),
        "n=(Z sigma/d)^2",ref_lw,c("sd","d")),
      M("sens_precision","Diagnostic accuracy","Estimate sensitivity","Precision-based sample size for sensitivity, inflated to total recruitment using disease prevalence.","sensitivity_precision",
        list(I("sensitivity","Expected sensitivity",0.90,"Anticipated sensitivity.",min=0.001,max=0.999,step=0.01),I("prevalence","Disease prevalence",0.20,"Expected target-condition prevalence in recruited participants.",min=0.001,max=0.999,step=0.01),I("d","Absolute precision",0.05,"Desired margin of error for sensitivity.",min=0.001,max=0.5,step=0.005),C$alpha),
        "n+=Z^2 Se(1-Se)/d^2; N=n+/Prev","Buderer NM. Statistical methodology: incorporating prevalence into sample size calculation for sensitivity and specificity.",c("sensitivity","prevalence","d")),
      M("spec_precision","Diagnostic accuracy","Estimate specificity","Precision-based sample size for specificity, inflated to total recruitment using the non-diseased fraction.","specificity_precision",
        list(I("specificity","Expected specificity",0.90,"Anticipated specificity.",min=0.001,max=0.999,step=0.01),I("prevalence","Disease prevalence",0.20,"Expected target-condition prevalence.",min=0.001,max=0.999,step=0.01),I("d","Absolute precision",0.05,"Desired margin of error for specificity.",min=0.001,max=0.5,step=0.005),C$alpha),
        "n-=Z^2 Sp(1-Sp)/d^2; N=n-/(1-Prev)","Buderer NM. Statistical methodology: incorporating prevalence into sample size calculation for sensitivity and specificity.",c("specificity","prevalence","d")),
      M("auc_precision","Diagnostic accuracy","Estimate AUC with desired CI width","Precision-based sample size for an ROC AUC.","auc_precision",
        list(I("auc","Expected AUC",0.75,"Anticipated area under the ROC curve.",min=0.5001,max=0.999,step=0.01),I("prevalence","Disease prevalence",0.30,"Fraction of cases in the study sample.",min=0.001,max=0.999,step=0.01),I("conf_width","Full CI width",0.10,"Desired full width of the confidence interval.",min=0.001,max=0.9,step=0.01),C$alpha),
        "Precision solved from the Hanley-McNeil AUC variance.","Hanley JA, McNeil BJ. Radiology. 1982.",c("auc","prevalence","conf_width"),"NUMERICAL",package="presize"),
      M("cor_precision","Estimation & precision","Estimate Pearson correlation","Precision-based sample size for Pearson correlation.","cor_precision_pearson",
        list(I("r","Expected correlation",0.40,"Expected Pearson correlation.",min=-0.99,max=0.99,step=0.05),I("conf_width","Full CI width",0.20,"Desired full confidence-interval width.",min=0.01,max=1.9,step=0.01),C$alpha),
        "Bonett-Wright correlation precision equation.","Bonett DG, Wright TA. Psychometrika. 2000.",c("r","conf_width"),"NUMERICAL",package="presize"),
      M("spearman_precision","Estimation & precision","Estimate Spearman correlation","Precision-based sample size for Spearman correlation.","cor_precision_spearman",
        list(I("r","Expected Spearman correlation",0.40,"Expected rank correlation.",min=-0.99,max=0.99,step=0.05),I("conf_width","Full CI width",0.20,"Desired full CI width.",min=0.01,max=1.9,step=0.01),C$alpha),
        "Bonett-Wright rank-correlation precision equation.","Bonett DG, Wright TA. Psychometrika. 2000.",c("r","conf_width"),"NUMERICAL",package="presize"),
      M("kendall_precision","Estimation & precision","Estimate Kendall correlation","Precision-based sample size for Kendall's tau.","cor_precision_kendall",
        list(I("r","Expected Kendall tau",0.30,"Expected Kendall correlation.",min=-0.99,max=0.99,step=0.05),I("conf_width","Full CI width",0.20,"Desired full CI width.",min=0.01,max=1.9,step=0.01),C$alpha),
        "Bonett-Wright rank-correlation precision equation.","Bonett DG, Wright TA. Psychometrika. 2000.",c("r","conf_width"),"NUMERICAL",package="presize"),
      M("cronbach_precision","Scale, reliability & measurement","Cronbach's alpha - precision","Determine sample size for a desired confidence-interval width around Cronbach's alpha.","cronbach_precision",
        list(I("items","Number of items",10,"Number of scale items.",min=2,max=500,step=1),I("calpha","Expected Cronbach's alpha",0.80,"Expected reliability coefficient.",min=0.01,max=0.999,step=0.01),I("conf_width","Full CI width",0.10,"Desired full confidence-interval width.",min=0.01,max=0.9,step=0.01),C$alpha),
        "Bonett-Wright interval-estimation/sample-size method.",ref_bonett,c("items","calpha","conf_width"),"FORMULA/NUMERICAL",package="presize"),
      M("cronbach_test","Scale, reliability & measurement","Cronbach's alpha - hypothesis test","Test whether reliability exceeds a prespecified unacceptable value with desired power.","stat_cronbach_test",
        list(I("items","Number of items",10,"Number of items/measurements in the reliability coefficient.",min=2,max=1000,step=1),I("expected_alpha","Expected Cronbach's alpha",0.80,"Reliability expected under the alternative hypothesis.",min=0.01,max=0.999,step=0.01),I("null_alpha","Null / unacceptable alpha",0.70,"Reliability under the null hypothesis.",min=0,max=0.99,step=0.01),C$alpha,C$power),
        "Bonett-Wright reliability hypothesis-test equation.",ref_bonett,c("expected_alpha","null_alpha","items","power"),"POWER-BASED",package="statpsych"),
      M("cronbach_compare_test","Scale, reliability & measurement","Compare two Cronbach's alpha coefficients - power","Sample size per group for testing a difference between two reliability coefficients.","stat_cronbach2_test",
        list(I("items","Number of items",8,"Number of items/measurements.",min=2,max=1000,step=1),I("alpha1","Expected alpha, group 1",0.85,"Planning reliability for group 1.",min=0.01,max=0.999,step=0.01),I("alpha2","Expected alpha, group 2",0.70,"Planning reliability for group 2.",min=0.01,max=0.999,step=0.01),C$alpha,C$power),
        "Bonett-Wright two-group reliability test.",ref_bonett,c("alpha1","alpha2","items","power"),"POWER-BASED",package="statpsych"),
      M("cronbach_compare_precision","Scale, reliability & measurement","Compare two Cronbach's alpha coefficients - precision","Sample size per group to estimate a difference between Cronbach alpha coefficients with desired CI width.","stat_cronbach2_ci",
        list(I("items","Number of items",8,"Number of items/measurements.",min=2,max=1000,step=1),I("alpha1","Expected alpha, group 1",0.85,"Planning reliability for group 1.",min=0.01,max=0.999,step=0.01),I("alpha2","Expected alpha, group 2",0.70,"Planning reliability for group 2.",min=0.01,max=0.999,step=0.01),I("conf_width","Full CI width",0.15,"Desired full confidence-interval width for the alpha difference.",min=0.001,max=1.5,step=0.01),C$alpha),
        "Bonett-Wright two-group reliability precision equation.",ref_bonett,c("alpha1","alpha2","items","conf_width"),"PRECISION-BASED",package="statpsych"),
      M("icc_precision","Scale, reliability & measurement","ICC - precision","Determine subjects needed for a desired CI width around an ICC.","icc_precision",
        list(I("icc","Expected ICC",0.80,"Expected intraclass correlation.",min=0.01,max=0.99,step=0.01),I("raters","Measurements / raters per subject",2,"Number of repeated observations per subject.",min=2,max=100,step=1),I("conf_width","Full CI width",0.20,"Desired full confidence-interval width.",min=0.01,max=0.9,step=0.01),C$alpha),
        "Bonett ICC precision approximation.","Bonett DG. Statistics in Medicine. 2002;21:1331-1335.",c("icc","raters","conf_width"),"FORMULA/NUMERICAL",package="presize"),
      M("kappa_precision","Scale, reliability & measurement","Cohen/Fleiss kappa - precision","Precision-based sample size for categorical agreement.","kappa_precision",
        list(I("kappa","Expected kappa",0.60,"Expected kappa coefficient.",min=-0.9,max=0.99,step=0.05),I("raters","Number of raters",2,"Number of raters.",min=2,max=6,step=1),I("prop_cat1","Expected proportion in category 1",0.50,"Marginal proportion for category 1 in a binary rating.",min=0.01,max=0.99,step=0.05),I("conf_width","Full CI width",0.20,"Desired full CI width.",min=0.01,max=1.5,step=0.01),C$alpha),
        "Precision solved through kappaSize methods.","kappaSize / presize methodology.",c("kappa","prop_cat1","conf_width"),"NUMERICAL",package="presize"),
      M("bland_altman_precision","Scale, reliability & measurement","Bland-Altman limits of agreement - precision","Sample size based on desired precision around limits of agreement.","bland_altman_precision",
        list(I("conf_width","Full CI width in SD units",0.20,"Desired CI width around a limit of agreement, expressed in SD units.",min=0.01,max=2,step=0.01),C$alpha),
        "Bland-Altman precision equation.","Bland JM, Altman DG. Lancet. 1986.",c("conf_width"),"FORMULA/NUMERICAL",package="presize"),
      M("positive_lr_precision","Diagnostic accuracy","Positive likelihood ratio - precision","Precision-based sample size for LR+.","pos_lr_precision",
        list(I("prevalence","Disease prevalence",0.30,"Expected prevalence.",min=0.01,max=0.99,step=0.01),I("sensitivity","Sensitivity",0.85,"Expected sensitivity.",min=0.01,max=0.99,step=0.01),I("specificity","Specificity",0.80,"Expected specificity.",min=0.01,max=0.99,step=0.01),I("conf_width","Full CI width",1.0,"Desired full confidence interval width for LR+.",min=0.01,max=100,step=0.1),C$alpha),
        "Precision equation for likelihood ratios.","presize likelihood-ratio precision method.",c("sensitivity","specificity","prevalence"),"NUMERICAL",package="presize"),
      M("negative_lr_precision","Diagnostic accuracy","Negative likelihood ratio - precision","Precision-based sample size for LR-.","neg_lr_precision",
        list(I("prevalence","Disease prevalence",0.30,"Expected prevalence.",min=0.01,max=0.99,step=0.01),I("sensitivity","Sensitivity",0.85,"Expected sensitivity.",min=0.01,max=0.99,step=0.01),I("specificity","Specificity",0.80,"Expected specificity.",min=0.01,max=0.99,step=0.01),I("conf_width","Full CI width",0.20,"Desired full confidence interval width for LR-.",min=0.001,max=10,step=0.01),C$alpha),
        "Precision equation for likelihood ratios.","presize likelihood-ratio precision method.",c("sensitivity","specificity","prevalence"),"NUMERICAL",package="presize"),

      M("cv_precision","Estimation & precision","Coefficient of variation - precision","Estimate a coefficient of variation with a desired confidence-interval width.","stat_cv_ci",
        list(I("cv","Expected coefficient of variation",0.25,"Planning value of the coefficient of variation.",min=0.001,max=10,step=0.01),I("conf_width","Full CI width",0.10,"Desired confidence-interval width.",min=0.001,max=10,step=0.01),C$alpha),
        "Approximate CV precision equation.","Bonett DG. Statistical Methods for Psychologists.",c("cv","conf_width"),"PRECISION-BASED",package="statpsych"),
      M("eta2_precision","Estimation & precision","Eta-squared - precision","Estimate eta-squared in a one-way ANOVA with desired CI precision.","stat_etasqr_ci",
        list(I("eta2","Expected eta-squared",0.10,"Planning eta-squared value.",min=0.0001,max=0.99,step=0.01),I("groups","Number of groups",3,"Number of ANOVA groups.",min=2,max=100,step=1),I("conf_width","Full CI width",0.10,"Desired confidence-interval width.",min=0.001,max=1,step=0.01),C$alpha),
        "Eta-squared precision equation.","Bonett DG. Statistical Methods for Psychologists.",c("eta2","groups","conf_width"),"PRECISION-BASED",package="statpsych"),
      M("two_cor_precision","Estimation & precision","Difference between two correlations - precision","Estimate the difference between two independent Pearson correlations with desired CI width.","stat_cor2_ci",
        list(I("r1","Correlation group 1",0.80,"Planning correlation group 1.",min=-0.99,max=0.99,step=0.05),I("r2","Correlation group 2",0.50,"Planning correlation group 2.",min=-0.99,max=0.99,step=0.05),I("conf_width","Full CI width",0.20,"Desired confidence interval width for the difference.",min=0.001,max=2,step=0.01),C$alpha),
        "Fisher-transformation precision equation.","Bonett DG, Wright TA. Psychometrika 2000.",c("r1","r2","conf_width"),"PRECISION-BASED",package="statpsych"),
      M("two_mean_precision","Estimation & precision","Difference between two means - precision","Estimate a two-group mean difference with desired confidence-interval width and optional unequal allocation.","stat_mean2_ci",
        list(I("variance","Average within-group variance",100,"Planning average within-group variance.",min=0.0001,max=1e12),I("conf_width","Full CI width",12,"Desired confidence-interval width for the difference.",min=0.0001,max=1e12),C$ratio,C$alpha),
        "Two-group mean-difference precision equation.","Bonett DG. Statistical Methods for Psychologists.",c("variance","conf_width","ratio"),"PRECISION-BASED",package="statpsych"),

      M("one_prop_test","Group comparisons","One proportion versus a reference proportion","Power-based one-sample proportion comparison.","one_prop_test",
        list(I("p0","Reference / null proportion",0.50,"Null proportion.",min=0.001,max=0.999,step=0.01),I("p1","Expected true proportion",0.65,"Expected alternative proportion.",min=0.001,max=0.999,step=0.01),C$alpha,C$power),
        "Normal-approximation one-proportion power equation.",ref_chow,c("p0","p1","power")),
      M("two_prop","Group comparisons","Two independent proportions","Compare two independent proportions with optional unequal allocation.","two_prop",
        list(I("p1","Group 1 proportion",0.20,"Expected proportion in group 1.",min=0.001,max=0.999,step=0.01),I("p2","Group 2 proportion",0.10,"Expected proportion in group 2.",min=0.001,max=0.999,step=0.01),C$ratio,C$alpha,C$power),
        "Two-proportion normal-approximation equation.",ref_chow,c("p1","p2","power","ratio")),
      M("paired_prop","Group comparisons","Paired proportions / McNemar","Power calculation based on expected discordant paired proportions.","paired_prop",
        list(I("p01","P(first=0, second=1)",0.20,"Expected discordant proportion 0->1.",min=0,max=1,step=0.01),I("p10","P(first=1, second=0)",0.10,"Expected discordant proportion 1->0.",min=0,max=1,step=0.01),C$alpha,C$power),
        "McNemar discordant-pair equation.",ref_chow,c("p01","p10","power")),
      M("one_mean_test","Group comparisons","One mean versus reference","One-sample mean comparison.","one_mean_test",
        list(I("sd","Standard deviation",10,"Expected SD.",min=0.001,max=1e9),I("delta","Mean difference",5,"Expected difference from the reference mean.",min=0.0001,max=1e9),C$alpha,C$power),
        "Normal-approximation one-mean equation.",ref_chow,c("sd","delta","power")),
      M("two_mean","Group comparisons","Two independent means","Compare two independent means, including unequal allocation.","two_mean",
        list(I("sd","Pooled standard deviation",10,"Expected common SD.",min=0.001,max=1e9),I("delta","Mean difference",5,"Expected mean difference.",min=0.0001,max=1e9),C$ratio,C$alpha,C$power),
        "Normal-approximation two-mean equation.",ref_chow,c("sd","delta","power","ratio")),
      M("paired_mean","Group comparisons","Paired means / before-after","Compare paired quantitative measurements using SD of paired differences.","paired_mean",
        list(I("sd_diff","SD of paired differences",8,"Expected standard deviation of within-person differences.",min=0.001,max=1e9),I("delta","Mean paired difference",4,"Expected mean difference.",min=0.0001,max=1e9),C$alpha,C$power),
        "Paired-mean equation.",ref_chow,c("sd_diff","delta","power")),
      M("correlation_test","Group comparisons","Test one correlation","Test an expected correlation against a null correlation using Fisher's transformation.","correlation_test",
        list(I("r0","Null correlation",0,"Correlation under H0.",min=-0.99,max=0.99,step=0.05),I("r1","Expected correlation",0.30,"Expected correlation under H1.",min=-0.99,max=0.99,step=0.05),C$alpha,C$power),
        "Fisher-z power equation.","Fisher z transformation; standard power analysis for correlation.",c("r1","power")),
      M("two_correlations","Group comparisons","Compare two independent correlations","Compare correlation coefficients from two independent groups.","two_correlations",
        list(I("r1","Correlation group 1",0.30,"Expected correlation in group 1.",min=-0.99,max=0.99,step=0.05),I("r2","Correlation group 2",0.60,"Expected correlation in group 2.",min=-0.99,max=0.99,step=0.05),C$alpha,C$power),
        "Fisher-z comparison equation.","Fisher z transformation.",c("r1","r2","power")),
      M("two_rates","Epidemiologic studies","Compare two incidence rates","Approximate person-time requirement for two independent Poisson rates.","two_rates",
        list(I("rate1","Rate group 1",0.10,"Expected event rate per exposure-time unit.",min=1e-6,max=1e9),I("rate2","Rate group 2",0.05,"Expected event rate per exposure-time unit.",min=1e-6,max=1e9),C$ratio,C$alpha,C$power),
        "Poisson-rate normal approximation.",ref_chow,c("rate1","rate2","power")),
      M("logrank_hr","Survival & time-to-event","Two-group survival / log-rank using hazard ratio","Calculate required events and total sample from an expected HR and event fraction.","logrank_hr",
        list(I("hr","Expected hazard ratio",0.70,"Hazard ratio between groups.",min=0.01,max=20,step=0.05),I("allocation","Fraction allocated to group 1",0.50,"Allocation fraction to group 1.",min=0.01,max=0.99,step=0.05),I("event_fraction","Expected overall event fraction",0.40,"Expected fraction experiencing the event during follow-up.",min=0.01,max=0.99,step=0.05),C$alpha,C$power),
        "Schoenfeld event-based log-rank equation.","Schoenfeld DA. Sample-size formula for proportional-hazards regression.",c("hr","event_fraction","power")),
      M("anova","Group comparisons","One-way ANOVA - continuous outcome","Cohen-f sample-size calculation for an overall one-way ANOVA.","wp_anova",
        list(I("groups","Number of groups",3,"Number of independent groups.",min=2,max=100,step=1),I("f","Cohen's f",0.25,"ANOVA effect size f.",min=0.001,max=5,step=0.05),C$alpha,C$power),
        "Noncentral-F power equation.",ref_web,c("f","groups","power"),"POWER-BASED",package="WebPower"),
      M("anova_binary","Group comparisons","One-way comparison with binary outcome","Overall test across three or more groups with binary outcome.","wp_anova_binary",
        list(I("groups","Number of groups",3,"Number of groups.",min=2,max=100,step=1),I("effect","Effect size V",0.20,"Mai-Zhang effect size for binary analogous ANOVA.",min=0.001,max=5,step=0.05),C$alpha,C$power),
        "Likelihood-ratio power equation for binary analogous ANOVA.",ref_web,c("effect","groups","power"),"POWER-BASED",package="WebPower"),
      M("anova_count","Group comparisons","One-way comparison with count outcome","Overall test across groups with Poisson/count outcome.","wp_anova_count",
        list(I("groups","Number of groups",3,"Number of groups.",min=2,max=100,step=1),I("effect","Effect size V",0.20,"Mai-Zhang effect size for count analogous ANOVA.",min=0.001,max=5,step=0.05),C$alpha,C$power),
        "Likelihood-ratio power equation for count analogous ANOVA.",ref_web,c("effect","groups","power"),"POWER-BASED",package="WebPower"),

      M("linear_regression","Regression & multivariable models","Multiple linear regression","Sample size for testing a set of predictors using Cohen's f-squared.","wp_regression",
        list(I("tested_predictors","Predictor parameters tested",3,"Number of predictor parameters in the tested set.",min=1,max=1000,step=1),I("control_predictors","Control predictor parameters",0,"Number of additional predictors controlled for.",min=0,max=1000,step=1),I("f2","Cohen's f-squared",0.15,"Incremental regression effect size.",min=0.0001,max=10,step=0.05),C$alpha,C$power),
        "Noncentral-F regression power equation.",ref_web,c("f2","tested_predictors","power"),"POWER-BASED",package="WebPower"),
      M("logistic_regression","Regression & multivariable models","Logistic regression - one predictor effect","Demidenko power/sample size for a logistic-regression coefficient.","wp_logistic",
        list(I("p0","Outcome probability at reference predictor",0.15,"Baseline outcome probability.",min=0.001,max=0.999,step=0.01),I("p1","Outcome probability at comparison predictor",0.10,"Outcome probability at a comparison predictor value.",min=0.001,max=0.999,step=0.01),I("predictor_family","Predictor distribution","Bernoulli","Distribution of the predictor.",type="select",choices=c("Bernoulli","normal")),I("predictor_parameter","Bernoulli predictor prevalence",0.50,"Used when predictor family is Bernoulli.",min=0.001,max=0.999,step=0.05),C$alpha,C$power),
        "Demidenko logistic-regression power equation.","Demidenko E. Statistics in Medicine. 2007.",c("p0","p1","power"),"POWER-BASED",package="WebPower"),
      M("poisson_regression","Regression & multivariable models","Poisson regression - one predictor effect","Power/sample size for a Poisson-regression coefficient.","wp_poisson",
        list(I("base_rate","Base event rate",2,"Expected baseline Poisson mean/rate.",min=0.0001,max=1e6),I("rate_ratio","Relative rate",0.80,"Relative increase/multiplicative rate parameter used by WebPower.",min=0.001,max=100,step=0.05),I("predictor_parameter","Binary predictor prevalence",0.50,"Prevalence of Bernoulli predictor.",min=0.001,max=0.999,step=0.05),C$alpha,C$power),
        "Demidenko Poisson-regression power equation.","Demidenko E.; WebPower implementation.",c("base_rate","rate_ratio","power"),"POWER-BASED",package="WebPower"),
      M("mediation","Regression & multivariable models","Simple mediation effect","Power/sample size for a*b mediation using the WebPower Sobel-based method.","wp_mediation",
        list(I("a","Path a",0.30,"Path coefficient X to mediator.",min=-5,max=5,step=0.05),I("b","Path b",0.30,"Path coefficient mediator to outcome.",min=-5,max=5,step=0.05),I("varx","Variance of X",1,"Variance of predictor.",min=0.0001,max=1e6),I("varm","Variance of mediator",1,"Variance of mediator.",min=0.0001,max=1e6),I("vary","Variance of outcome",1,"Variance of outcome.",min=0.0001,max=1e6),C$alpha,C$power),
        "Sobel-based mediation power equation.","Sobel; MacKinnon; WebPower implementation.",c("a","b","power"),"POWER-BASED",package="WebPower"),
      M("rmanova","Longitudinal & repeated measures","Repeated-measures ANOVA","Power/sample size for repeated-measures ANOVA.","wp_rmanova",
        list(I("groups","Number of groups",2,"Between-subject groups.",min=1,max=20,step=1),I("measurements","Repeated measurements",4,"Number of repeated occasions.",min=2,max=100,step=1),I("f","Cohen's f",0.25,"Effect size f.",min=0.001,max=5,step=0.05),I("nonsphericity","Nonsphericity correction",1,"Population sphericity correction coefficient.",min=0.01,max=1,step=0.05),C$alpha,C$power),
        "Repeated-measures noncentral-F power equation.",ref_web,c("f","measurements","nonsphericity","power"),"POWER-BASED",package="WebPower"),
      M("cfa_rmsea","Scale, reliability & measurement","CFA / SEM - RMSEA power","MacCallum-Browne-Sugawara RMSEA-based SEM power/sample-size calculation.","wp_sem_rmsea",
        list(I("df","Model degrees of freedom",50,"Chi-square model degrees of freedom.",min=1,max=100000,step=1),I("rmsea0","RMSEA under H0",0.05,"Null/close-fit RMSEA.",min=0,max=1,step=0.005),I("rmsea1","RMSEA under H1",0.08,"Alternative RMSEA.",min=0,max=1,step=0.005),I("fit_test","Test type","close","Close-fit or not-close-fit test.",type="select",choices=c("close","notclose")),C$alpha,C$power),
        "Noncentral-chi-square RMSEA power equation.","MacCallum RC, Browne MW, Sugawara HM. Psychological Methods. 1996.",c("df","rmsea0","rmsea1","power"),"POWER-BASED",package="WebPower"),
      M("efa_rule","Scale, reliability & measurement","EFA planning guideline","Transparent item-to-subject planning guideline. Marked as a rule of thumb, not a universal factor-recovery formula.","efa_guidance",
        list(I("items","Number of items",20,"Number of items entering EFA.",min=2,max=1000,step=1),I("subjects_per_item","Subjects per item",10,"Rule-of-thumb ratio chosen by the investigator.",min=1,max=100,step=1),I("minimum_n","Minimum total sample",200,"Minimum sample floor.",min=20,max=100000,step=10)),
        "N=max(items x ratio, minimum N)","Use simulation/factor-recovery planning when possible; simple item ratios are not universal.",c("items","subjects_per_item"),"RULE OF THUMB"),
      M("cluster_adjustment","Cluster & multilevel designs","Design-effect inflation","Inflate an SRS sample size using average cluster size and ICC.","cluster_deff",
        list(I("base_n","Base SRS sample size",384,"Sample size before cluster inflation.",min=1,max=1e9),I("cluster_size","Average cluster size",20,"Average number of participants per cluster.",min=1,max=1e9),I("icc","Intracluster correlation",0.03,"Expected ICC.",min=0,max=0.999,step=0.01)),
        "DEFF=1+(m-1)ICC", "Standard cluster design-effect approximation.",c("cluster_size","icc")),
      M("crt_two_arm","Cluster & multilevel designs","Two-arm cluster randomized trial","Power/sample-size calculation for a two-arm cluster randomized trial.","wp_crt2arm",
        list(I("f","Standardized treatment effect",0.40,"Effect size / mean difference parameter used by WebPower CRT method.",min=0.001,max=10,step=0.05),I("clusters_per_arm","Clusters per arm",10,"Number of clusters on each side/arm.",min=2,max=10000,step=1),I("icc","Intracluster correlation",0.05,"ICC.",min=0,max=0.99,step=0.01),C$alpha,C$power),
        "Two-level CRT power equation.",ref_web,c("f","clusters_per_arm","icc","power"),"POWER-BASED",package="WebPower"),

      M("trial_mean_superiority","Clinical trials","Two-sample mean - superiority/equality","Clinical-trial sample size for two independent means.","TrialSize::TwoSampleMean.Equality",
        list(I("sd","Pooled SD",1,"Pooled standard deviation.",min=0.001,max=1e9),I("delta","True mean difference",0.5,"Expected treatment-control mean difference.",min=-1e9,max=1e9),C$ratio,C$alpha,C$power),
        "Chow-Shao-Wang two-sample mean equation.",ref_chow,c("sd","delta","power"),"POWER-BASED",package="TrialSize"),
      M("trial_mean_ni","Clinical trials","Two-sample mean - non-inferiority","Clinical-trial non-inferiority sample size for means.","TrialSize::TwoSampleMean.NIS",
        list(I("sd","Pooled SD",1,"Pooled standard deviation.",min=0.001,max=1e9),I("expected_diff","Expected mean difference",0,"Expected treatment-control difference.",min=-1e9,max=1e9),I("margin","NI margin",-0.5,"Non-inferiority margin using TrialSize sign convention.",min=-1e9,max=1e9),C$ratio,C$alpha,C$power),
        "Chow-Shao-Wang NI mean equation.",ref_chow,c("expected_diff","margin","power"),"POWER-BASED",package="TrialSize"),
      M("trial_mean_equiv","Clinical trials","Two-sample mean - equivalence","Clinical-trial equivalence sample size for means.","TrialSize::TwoSampleMean.Equivalence",
        list(I("sd","Pooled SD",1,"Pooled standard deviation.",min=0.001,max=1e9),I("expected_diff","Expected mean difference",0,"Expected true difference.",min=-1e9,max=1e9),I("margin","Equivalence margin",0.5,"Equivalence bound.",min=0.0001,max=1e9),C$ratio,C$alpha,C$power),
        "Chow-Shao-Wang equivalence equation.",ref_chow,c("expected_diff","margin","power"),"POWER-BASED",package="TrialSize"),
      M("trial_prop_superiority","Clinical trials","Two-sample proportion - superiority/equality","Clinical-trial sample size for two proportions.","TrialSize::TwoSampleProportion.Equality",
        list(I("p1","Treatment proportion",0.65,"Expected response proportion treatment.",min=0.001,max=0.999,step=0.01),I("p2","Control proportion",0.85,"Expected response proportion control.",min=0.001,max=0.999,step=0.01),C$ratio,C$alpha,C$power),
        "Chow-Shao-Wang two-proportion equation.",ref_chow,c("p1","p2","power"),"POWER-BASED",package="TrialSize"),
      M("trial_prop_ni","Clinical trials","Two-sample proportion - non-inferiority","Clinical-trial non-inferiority sample size for proportions.","TrialSize::TwoSampleProportion.NIS",
        list(I("p1","Treatment proportion",0.80,"Expected treatment proportion.",min=0.001,max=0.999,step=0.01),I("p2","Control proportion",0.85,"Expected control proportion.",min=0.001,max=0.999,step=0.01),I("expected_diff","Expected p1-p2",-0.05,"Expected difference.",min=-0.999,max=0.999,step=0.01),I("margin","NI margin",-0.10,"Non-inferiority margin using TrialSize sign convention.",min=-0.999,max=0.999,step=0.01),C$ratio,C$alpha,C$power),
        "Chow-Shao-Wang NI proportion equation.",ref_chow,c("p1","p2","margin","power"),"POWER-BASED",package="TrialSize"),
      M("trial_prop_equiv","Clinical trials","Two-sample proportion - equivalence","Clinical-trial equivalence sample size for proportions.","TrialSize::TwoSampleProportion.Equivalence",
        list(I("p1","Treatment proportion",0.80,"Expected treatment proportion.",min=0.001,max=0.999,step=0.01),I("p2","Control proportion",0.82,"Expected control proportion.",min=0.001,max=0.999,step=0.01),I("expected_diff","Expected p1-p2",-0.02,"Expected difference.",min=-0.999,max=0.999,step=0.01),I("margin","Equivalence margin",0.10,"Equivalence bound.",min=0.001,max=0.999,step=0.01),C$ratio,C$alpha,C$power),
        "Chow-Shao-Wang equivalence proportion equation.",ref_chow,c("p1","p2","margin","power"),"POWER-BASED",package="TrialSize"),
      M("crossover_mean","Special designs","Two-period crossover - quantitative outcome","Paired/crossover sample-size calculation based on within-person SD.","crossover_mean",
        list(I("sd_diff","Within-person SD of treatment difference",1,"SD of paired treatment differences.",min=0.001,max=1e9),I("delta","Expected treatment difference",0.5,"Expected mean treatment difference.",min=0.0001,max=1e9),C$alpha,C$power),
        "Paired treatment-difference equation.",ref_chow,c("sd_diff","delta","power")),
      M("crossover_trial_equiv","Special designs","Two-period crossover - equivalence","TrialSize clinical-trial equivalence calculation for crossover means.","TrialSize::TwoSampleCrossOver.Equivalence",
        list(I("sd_diff","Crossover SD",0.20,"Within-subject crossover SD.",min=0.0001,max=1e9),I("expected_diff","Expected difference",-0.10,"Expected treatment difference.",min=-1e9,max=1e9),I("margin","Equivalence margin",0.25,"Equivalence margin.",min=0.0001,max=1e9),C$alpha,C$power),
        "Chow-Shao-Wang crossover equivalence equation.",ref_chow,c("sd_diff","margin","power"),"POWER-BASED",package="TrialSize"),

      M("trial_one_mean_superiority","Clinical trials","One-sample mean - superiority/equality","Clinical-trial sample size for a one-sample mean test.","TrialSize::OneSampleMean.Equality",
        list(I("sd","Standard deviation",1,"Outcome SD.",min=0.001,max=1e9),I("delta","Mean difference from reference",0.5,"Expected difference from the null/reference mean.",min=-1e9,max=1e9),C$alpha,C$power),
        "Chow-Shao-Wang one-sample mean equation.",ref_chow,c("sd","delta","power"),"POWER-BASED",package="TrialSize"),
      M("trial_one_mean_ni","Clinical trials","One-sample mean - non-inferiority","One-sample non-inferiority clinical-trial calculation.","TrialSize::OneSampleMean.NIS",
        list(I("sd","Standard deviation",1,"Outcome SD.",min=0.001,max=1e9),I("expected_diff","Expected difference",0,"Expected difference.",min=-1e9,max=1e9),I("margin","NI margin",-0.5,"Non-inferiority margin using TrialSize sign convention.",min=-1e9,max=1e9),C$alpha,C$power),
        "Chow-Shao-Wang one-sample NI equation.",ref_chow,c("expected_diff","margin","power"),"POWER-BASED",package="TrialSize"),
      M("trial_one_mean_equiv","Clinical trials","One-sample mean - equivalence","One-sample equivalence clinical-trial calculation.","TrialSize::OneSampleMean.Equivalence",
        list(I("sd","Standard deviation",1,"Outcome SD.",min=0.001,max=1e9),I("expected_diff","Expected difference",0,"Expected true difference.",min=-1e9,max=1e9),I("margin","Equivalence margin",0.5,"Equivalence bound.",min=0.0001,max=1e9),C$alpha,C$power),
        "Chow-Shao-Wang one-sample equivalence equation.",ref_chow,c("expected_diff","margin","power"),"POWER-BASED",package="TrialSize"),
      M("crossover_trial_superiority","Special designs","Two-period crossover - superiority/equality","TrialSize crossover equality/superiority calculation.","TrialSize::TwoSampleCrossOver.Equality",
        list(I("sd_diff","Crossover SD",0.20,"Within-subject SD in crossover design.",min=0.0001,max=1e9),I("delta","Expected treatment difference",0.10,"Expected treatment difference.",min=-1e9,max=1e9),C$alpha,C$power),
        "Chow-Shao-Wang crossover equality equation.",ref_chow,c("sd_diff","delta","power"),"POWER-BASED",package="TrialSize"),
      M("crossover_trial_ni","Special designs","Two-period crossover - non-inferiority","TrialSize crossover non-inferiority calculation.","TrialSize::TwoSampleCrossOver.NIS",
        list(I("sd_diff","Crossover SD",0.20,"Within-subject SD.",min=0.0001,max=1e9),I("expected_diff","Expected difference",0,"Expected difference.",min=-1e9,max=1e9),I("margin","NI margin",-0.20,"Non-inferiority margin using TrialSize sign convention.",min=-1e9,max=1e9),C$alpha,C$power),
        "Chow-Shao-Wang crossover NI equation.",ref_chow,c("sd_diff","margin","power"),"POWER-BASED",package="TrialSize"),

      M("prediction_binary","Prediction models","Prediction model development - binary outcome","Riley et al. minimum sample size for developing a multivariable binary-outcome prediction model.","pms_binary",
        list(I("parameters","Candidate predictor parameters",20,"Number of candidate predictor parameters, not merely variables.",min=1,max=10000,step=1),I("rsquared","Expected Cox-Snell R-squared",0.15,"Anticipated Cox-Snell R-squared.",min=0.0001,max=0.99,step=0.01),I("prevalence","Outcome prevalence",0.20,"Expected binary outcome frequency.",min=0.001,max=0.999,step=0.01),I("shrinkage","Target shrinkage",0.90,"Target global shrinkage; 0.90 is commonly recommended.",min=0.50,max=0.999,step=0.01)),
        "Maximum across Riley et al. model-development criteria.","Riley RD et al. BMJ 2020; Statistics in Medicine 2018.",c("parameters","rsquared","prevalence","shrinkage"),"MULTI-CRITERION",package="pmsampsize"),
      M("prediction_continuous","Prediction models","Prediction model development - continuous outcome","Riley et al. sample size for a continuous-outcome prediction model.","pms_continuous",
        list(I("parameters","Candidate predictor parameters",20,"Candidate predictor parameters.",min=1,max=10000,step=1),I("rsquared","Expected R-squared",0.20,"Expected adjusted R-squared.",min=0.0001,max=0.99,step=0.01),I("mean_outcome","Expected outcome mean",10,"Expected population outcome mean.",min=-1e9,max=1e9),I("sd","Outcome SD",5,"Population outcome SD.",min=0.0001,max=1e9),I("shrinkage","Target shrinkage",0.90,"Desired shrinkage.",min=0.50,max=0.999,step=0.01)),
        "Maximum across Riley et al. continuous-model criteria.","Riley RD et al. Statistics in Medicine 2018; BMJ 2020.",c("parameters","rsquared","shrinkage"),"MULTI-CRITERION",package="pmsampsize"),
      M("prediction_survival","Prediction models","Prediction model development - time-to-event outcome","Riley et al. sample size for a survival prediction model.","pms_survival",
        list(I("parameters","Candidate predictor parameters",20,"Candidate predictor parameters.",min=1,max=10000,step=1),I("rsquared","Expected Cox-Snell R-squared",0.10,"Expected Cox-Snell R-squared.",min=0.0001,max=0.99,step=0.01),I("event_rate","Event rate per time unit",0.10,"Overall event rate in the target population.",min=0.000001,max=1e6),I("timepoint","Prediction timepoint",2,"Timepoint of interest.",min=0.0001,max=1e6),I("mean_followup","Mean follow-up",2.5,"Mean follow-up in same time units.",min=0.0001,max=1e6),I("shrinkage","Target shrinkage",0.90,"Desired shrinkage.",min=0.50,max=0.999,step=0.01)),
        "Maximum across Riley et al. survival-model criteria.","Riley RD et al. Statistics in Medicine 2018; BMJ 2020.",c("parameters","rsquared","event_rate","shrinkage"),"MULTI-CRITERION",package="pmsampsize")
    )
    cache
  }
})

.r4vn_ss_registry_table <- function() {
  x <- .r4vn_ss_registry()
  data.frame(
    id=vapply(x,`[[`,character(1),"id"),
    category=vapply(x,`[[`,character(1),"category"),
    name=vapply(x,`[[`,character(1),"name"),
    method_type=vapply(x,`[[`,character(1),"method_type"),
    package=vapply(x,function(z) z$package %||% "R4VN",character(1)),
    stringsAsFactors=FALSE
  )
}

.r4vn_ss_get_method <- function(id) {
  x <- .r4vn_ss_registry()
  hit <- which(vapply(x,function(z) identical(z$id,id),logical(1)))
  if(!length(hit)) stop("Unknown sample-size method.",call.=FALSE)
  x[[hit[1]]]
}

.r4vn_ss_sensitivity <- function(method, base_values, parameter, values,
                                 loss=0,design_effect=1,population=NA_real_) {
  if(!parameter %in% method$explore) stop("This parameter is not enabled for sensitivity analysis.",call.=FALSE)
  rows <- lapply(values,function(z){
    vv <- base_values; vv[[parameter]] <- z
    ans <- tryCatch(.r4vn_ss_calculate(method,vv,loss,design_effect,population),error=function(e) e)
    if(inherits(ans,"error")) data.frame(value=z,base_n=NA_real_,final_n=NA_real_,error=conditionMessage(ans))
    else data.frame(value=z,base_n=sum(ceiling(ans$group_n)),final_n=ans$final_n,error="")
  })
  do.call(rbind,rows)
}

.r4vn_ss_formula_preview <- function(method, values = NULL) {
  if (is.null(method) || !is.list(method)) return(NULL)
  if (is.null(values)) {
    values <- setNames(lapply(method$inputs, function(z) z$default),
                       vapply(method$inputs, `[[`, character(1), "id"))
  }

  direct_engines <- c(
    "prop_precision", "prop_relative_precision", "mean_precision",
    "sensitivity_precision", "specificity_precision", "one_prop_test",
    "two_prop", "paired_prop", "one_mean_test", "two_mean", "paired_mean",
    "correlation_test", "two_correlations", "two_rates", "logrank_hr",
    "cluster_deff", "efa_guidance", "ni_mean", "crossover_mean"
  )

  if (method$engine %in% direct_engines) {
    z <- tryCatch(.r4vn_ss_calc_direct(method$engine, values), error = function(e) NULL)
    if (!is.null(z)) {
      return(list(
        label = "Formula",
        formula = z$formula,
        substitution = z$substitution,
        math = TRUE,
        exact = TRUE
      ))
    }
  }

  f <- method$formula %||% ""
  is_math <- nzchar(f) && (grepl("=", f, fixed = TRUE) || grepl("\\\\", f) || grepl("\\^", f))
  list(
    label = if (is_math) "Formula / calculation rule" else "Calculation approach",
    formula = f,
    substitution = NULL,
    math = is_math,
    exact = FALSE
  )
}

Try the R4VN package in your browser

Any scripts or data that you put into this service are public.

R4VN documentation built on Sept. 30, 2026, 5:13 p.m.