- Overview
- Installation
- Statistical model (Huang et al. 2020; Sec. 2 of arXiv:2508.08445)
- Package contents (exported)
- Example: D-optimal design (Table 1, M = 61, q = 0)
- Example: A-optimal design (same theta, q = 0)
- Example: Cost depending on pool size (q > 0)
- Example: c-optimality
- Example: E-optimality via
compute_design_SO - Example: Equivalence theorem check (D-opt)
- Maximin multi-objective designs
- Tables and Figures in the paper
- References
- License
- TODO
Authors
Chi-Kuang Yeh (Georgia State University)
Weng Kee Wong (University of California, Los Angeles)
Julie Zhou (University of Victoria)
gtDesign is an R package for locally optimal approximate designs
on a finite candidate set for group testing (pooled testing).
Designs are found by convex optimization via
CVXR (typically with the
CLARABEL solver).
The main application is the Huang et al. (2020) model for
prevalence, sensitivity, and specificity with optional cost that
depends on pool size, as used in Yeh, Wong, and Zhou (2025); see
arXiv:2508.08445. The same interface
also supports generic nonlinear design whenever the (approximate)
information matrix is a sum of rank-one terms
# install.packages("remotes")
remotes::install_github("chikuang/gtDesign")# Alternative
devtools::install_github("chikuang/gtDesign")Requirements: R ≥ 4.0 recommended; packages CVXR, tibble,
MASS (see DESCRIPTION). A conic solver reachable by CVXR
(e.g. CLARABEL) is required at runtime.
Let
Standardized cost (assay + enrollment) is
so
The Fisher information matrix for
where
and
Many functions (calc_Dopt, calc_Aopt, check_equivalence, …) assume
a regression form
with no separate info_weight. That matches the Huang information
matrix if we set
The function gt_huang2020_regressor(theta, q) returns the function
function(x) that computes compute_design_SO(..., info_weight = ...) with raw
Local optimality: all designs are local with respect to a
nominal
| Area | Functions |
|---|---|
| Huang (2020) building blocks | gt_huang2020_pi, gt_huang2020_cost, gt_huang2020_lambda, gt_huang2020_f, gt_huang2020_regressor |
| Classical approximate designs | calc_Dopt, calc_Aopt, calc_copt |
| Single-objective (D, A, Ds, c, E) | compute_design_SO |
| Maximin multi-objective | maximin_design_workflow, compute_maximin_design, calc_eta_weights_maximin |
| Equivalence | check_equivalence, check_equivalence_maximin |
| Plots | plot_equivalence, plot_equivalence_maximin |
| Directional derivatives | calc_directional_derivatives, calc_multi_directional_derivative |
Nominal
library(gtDesign)
theta <- c(p0 = 0.07, p1 = 0.93, p2 = 0.96)
M <- 61L
u <- seq_len(M)
f <- gt_huang2020_regressor(theta, q = 0)
res_d <- calc_Dopt(u, f, drop_tol = 1e-6)
res_d$design |> round(3) point weight
1 1 0.333
2 17 0.333
3 61 0.333
res_d$status[1] "optimal"
Support points
res_a <- calc_Aopt(u, f, drop_tol = 1e-6)
res_a$design |> round(3) point weight
1 1 0.416
2 16 0.213
3 61 0.371
Larger f <- gt_huang2020_regressor(theta, q) with your chosen
f_q <- gt_huang2020_regressor(theta, q = 0.2)
res_d_q <- calc_Dopt(u, f_q, drop_tol = 1e-6)
res_d_q$design point weight
1 1 0.3333
2 10 0.3333
3 61 0.3333
Minimize the asymptotic variance of
c_vec <- c(0, 1, 1)
res_c <- calc_copt(u, f, cVec = c_vec, drop_tol = 1e-8)
subset(res_c$design, weight > 0.01) point weight
1 1 0.5213
4 56 0.1800
5 57 0.2988
res_e <- compute_design_SO(
u = u,
f = f,
criterion = "E",
solver = "CLARABEL"
)
res_e$design# A tibble: 3 × 2
point weight
<int> <dbl>
1 1 0.415
2 16 0.250
3 61 0.335
eq_d <- check_equivalence(res_d, f = f, tol = 1e-4)
eq_d$max_violation[1] 2.217e-05
eq_d$all_nonpositive[1] TRUE
plot_equivalence(eq_d, main = "D-opt: equivalence derivative")The maximin formulation (Sec. 4 of
arXiv:2508.08445) maximizes the
minimum efficiency across several criteria. The workflow matches the
regression-design package
cvxDesign (maximin
section): first
obtain reference losses from single-objective optimal designs on the
same candidate set u and regressor f, then call
compute_maximin_design(). For group testing, f is typically
gt_huang2020_regressor(theta, q); for polynomial regression, f can
be any function(x) returning a regressor vector (see cvxDesign
examples).
Chunks below call library(gtDesign). When you edit package source and
re-render this file, run devtools::install() (from the package
root) first so the README runs against the installed version.
Reference losses. Losses must be on the same scale as
compute_design_SO() / internal scalar_loss: for D, the loss is
calc_Dopt(), pass
D = -obj$value because calc_Dopt()$value stores
$\log\det(\mathbf{M})$). For A and c, calc_Aopt() /
calc_copt() use the same scalar loss as compute_design_SO().
The chunks below reuse u, f, res_d, and res_a from the D-opt and
A-opt examples.
loss_ref <- list(
D = -res_d$value,
A = res_a$value
)
loss_ref$D
[1] -5.796
$A
[1] 0.7058
res_da <- compute_maximin_design(
u = u,
f = f,
loss_ref = loss_ref,
criteria = c("D", "A")
)
res_da$design# A tibble: 4 × 2
point weight
<int> <dbl>
1 1 0.382
2 16 0.114
3 17 0.148
4 61 0.356
res_da$efficiency D A
0.987 0.987
res_da$tstar[1] 1.013
The efficiencies should be approximately equal at a maximin
solution; minimum efficiency equals tstar is
returned by the solver.
tol <- 1e-4
eq_eff <- abs(res_da$efficiency["D"] - res_da$efficiency["A"])
eq_t <- abs(min(res_da$efficiency) - 1 / res_da$tstar)
eq_eff < tol && eq_t < tol[1] TRUE
Parameter dimension is calc_eta_weights_maximin solves a small linear program; use the
same f (and cVec / opts for c) as in
compute_maximin_design(). If the default solver fails, try
solver = "SCS" and slightly relax tol (e.g. 1e-4); see
?calc_eta_weights_maximin.
dd_da <- calc_directional_derivatives(
u = u,
M = res_da$info_matrix,
f = f,
criteria = c("D", "A")
)
eta_da <- calc_eta_weights_maximin(
tstar = res_da$tstar,
loss_ref = loss_ref,
loss_model = res_da$loss,
directional_derivatives = dd_da,
criteria = c("D", "A"),
q = 3,
tol = 1e-4
)
eta_da D A
0.1898 0.6205
eqm_da <- check_equivalence_maximin(
design_obj = res_da,
directional_derivatives = dd_da,
eta = eta_da,
tol = 1e-3
)
eqm_da$all_nonpositive[1] TRUE
eqm_da$support_equal_zero[1] TRUE
plot_equivalence_maximin(
design_obj = res_da,
directional_derivatives = dd_da,
eta = eta_da,
criteria = c("D", "A")
)out <- maximin_design_workflow(
u = u,
f = f,
criteria = c("D", "A"),
make_figure = TRUE
)out$maximin$efficiency D A
0.987 0.987
out$maximin$value[1] 0.987
out$maximin$tstar[1] 1.013
out$maximin$design# A tibble: 4 × 2
point weight
<int> <dbl>
1 1 0.382
2 16 0.114
3 17 0.148
4 61 0.356
Use the same contrast as in the c-optimal example (c_vec). Pass
opts = list(cVec_c = c_vec) whenever c is included.
loss_ref_dac <- list(
D = -res_d$value,
A = res_a$value,
c = res_c$value
)
res_dac <- compute_maximin_design(
u = u,
f = f,
loss_ref = loss_ref_dac,
criteria = c("D", "A", "c"),
opts = list(cVec_c = c_vec)
)
res_dac$efficiency D A c
0.8907 0.9587 0.8907
Directional derivatives,
dd_dac <- calc_directional_derivatives(
u = u,
M = res_dac$info_matrix,
f = f,
criteria = c("D", "A", "c"),
cVec = c_vec
)
eta_dac <- calc_eta_weights_maximin(
tstar = res_dac$tstar,
loss_ref = loss_ref_dac,
loss_model = res_dac$loss,
directional_derivatives = dd_dac,
criteria = c("D", "A", "c"),
q = 3,
tol = 1e-3
)
check_equivalence_maximin(res_dac, dd_dac, eta_dac, tol = 0.002)$all_nonpositive[1] TRUE
plot_equivalence_maximin(
res_dac,
dd_dac,
eta_dac,
criteria = c("D", "A", "c")
)library(dplyr)
library(knitr)
theta <- c(p0 = 0.07, p1 = 0.93, p2 = 0.96)
u <- 1:61
q_val <- 0.8
f_q0 <- gt_huang2020_regressor(theta, q = q_val)
make_summary <- function(design, criterion) {
design |>
filter(weight > 1e-4) |>
select(1, 2) |>
rename(group_size = 1, weight = 2) |>
mutate(weight = round(weight, 3)) |>
summarise(
Criterion = criterion,
`Support points` = paste(group_size, collapse = ", "),
Weights = paste(weight, collapse = ", ")
)
}
tab_summary <- bind_rows(
make_summary(calc_Dopt(u, f_q0)$design, "D-opt"),
make_summary(calc_Aopt(u, f_q0)$design, "A-opt"),
make_summary(calc_copt(u, f_q0, cVec = c(1, 0, 0))$design, "D_s-opt"),
make_summary(calc_copt(u, f_q0, cVec = c(0, 1, 1))$design, "c-opt")
)
knitr::kable(
tab_summary,
format = "pipe",
caption = paste0(
"Support points and weights for approximate optimal designs ",
"for the Huang (2020) group testing model with q = ", q_val,
" on the design space {", min(u), ", ..., ", max(u), "}."
)
)| Criterion | Support points | Weights |
|---|---|---|
| D-opt | 1, 7, 8, 61 | 0.333, 0.029, 0.304, 0.333 |
| A-opt | 1, 8, 61 | 0.125, 0.183, 0.692 |
| D_s-opt | 1, 7, 61 | 0.095, 0.573, 0.332 |
| c-opt | 1, 56, 57 | 0.139, 0.322, 0.539 |
Support points and weights for approximate optimal designs for the Huang (2020) group testing model with q = 0.8 on the design space {1, …, 61}.
u <- seq_len(150L)
f_q <- gt_huang2020_regressor(theta, q = 0.0)
res_d_q <- calc_Dopt(u, f_q, drop_tol = 1e-6)
res_a_q <- calc_Aopt(u, f_q, drop_tol = 1e-6)
loss_ref <- list(
D = -res_d_q$value,
A = res_a_q$value
)
loss_ref$D
[1] -6.185
$A
[1] 0.5617
### Step 2: maximin design (D and A)
res_da <- compute_maximin_design(
u = u,
f = f_q,
loss_ref = loss_ref,
criteria = c("D", "A")
)
res_da$design# A tibble: 3 × 2
point weight
<int> <dbl>
1 1 0.409
2 19 0.251
3 150 0.339
res_da$efficiency D A
0.9805 0.9805
res_da$tstar[1] 1.02
### Step 3 numerical check
tol <- 1e-4
eq_eff <- abs(res_da$efficiency["D"] - res_da$efficiency["A"])
eq_t <- abs(min(res_da$efficiency) - 1 / res_da$tstar)
eq_eff < tol && eq_t < tol[1] TRUE
u <- seq_len(150L)
cVec <- c(1, 0, 0)
theta = c(0.07, 0.93, 0.96)
f_q <- gt_huang2020_regressor(theta, q = 0.2)
out <- maximin_design_workflow(
u = u,
f = f_q,
criteria = c("D", "A", "c"),
opts = list(cVec_c = cVec),
make_figure = TRUE
)out$maximin$efficiency D A c
0.9001 0.8545 0.8545
out$maximin$value[1] 0.8545
out$maximin$tstar[1] 1.17
out$maximin$design# A tibble: 3 × 2
point weight
<int> <dbl>
1 1 0.158
2 10 0.364
3 75 0.478
theta <- c(p0 = 0.07, p1 = 0.93, p2 = 0.96)
u <- seq_len(150L)
q_cost <- 0
f <- gt_huang2020_regressor(theta, q_cost)
# calculate the approximate design
res_D <- calc_Dopt(u, f, drop_tol = 1e-6)
# Table 3, C e.g., C = 100, 500, 10000
out <- round_gt_design_budget(
approx_design = res_D,
u = u,
theta = theta,
C = 100,
q_cost = q_cost,
criterion = "D"
)
out$C_remaining[1] 1
out$design_exact [,1] [,2] [,3] [,4] [,5]
xx 1 19 148 149 150
n_prime 32 33 1 1 33
out$efficiency [1] 0.9993
out$design_round1 [,1] [,2] [,3] [,4] [,5]
xx 1 19 148 149 150
n_prime 31 33 1 1 33
out$delta [,1]
xx 1
Delta_n 1
# 8*(1 - q_cost + q_cost*1 ) # 8
# 2*(1 - q_cost + q_cost*10) # 5.6
#
# my_cost <- 0
# for (i in 1:ncol(out$design_round1)) {
# my_cost <- out$design_round1[2, i] * (1 - q_cost + q_cost*out$design_round1[1,i]) + my_cost
# }-
Yeh, C.-K., Wong, W. K., Zhou, J. (2025). Single and multi-objective optimal designs for group testing experiments. arXiv 2508.08445. https://doi.org/10.48550/arXiv.2508.08445
-
Huang, S.-Y., Chen, Y.-H., Wang, W. (2020). Optimal group testing designs for estimating prevalence with imperfect tests. Journal of the Royal Statistical Society Series C.
-
Pukelsheim, F. (2006). Optimal Design of Experiments. SIAM.
-
Fedorov, V. V. (1972). Theory of Optimal Experiments. Academic Press.
GPL-3 — see the License field in DESCRIPTION.
- Finish up the package documentation and vignettes.
- Add the rounding algorithms to obtain the optimal exact designs






