Three ATA Strategies

Hong Chen

2026-03-18

Introduction

This vignette demonstrates multistage test (MST) assembly under three design strategies:

The examples mirror demonstration examples from the Rmst package (https://github.com/xluo11/Rmst/tree/master), using the same item pool and specifications. To ensure fast vignette rendering and CRAN compatibility, the full assembly process is not executed here. The assembled panel shown below was precomputed and saved as a package dataset.

The purpose of this vignette is not only to reproduce assembly results, but to highlight:

Item pool

The pool contains: 400 dichotomous 3PL items; 20 GPCM items and 20 GRM items. 127 items for content 1, 96 items for content 2, 103 items for content 3 and 114 items for content 4.

Each item includes: categorical attribute (model, content area) and continuous attribute (IRT parameters, response time).

data("Rmst_pool")
PoolSize<-nrow(Rmst_pool)
content_categories<-c("1","2","3","4")
item_par_cols<-list("3PL"=c("a","b","c"),
                    "GPCM" = c("alpha","delta1","delta2","delta3"),
                    "GRM" = c("alpha","beta1","beta2"))
Descriptive statistics for item quantitative attributes.
Attribute Mean SD Min Max
3PL: a 0.91 0.19 0.55 2.01
3PL: b -0.03 0.71 -2.04 2.09
3PL: c 0.10 0.04 0.02 0.20
GRM: alpha 0.94 0.16 0.72 1.26
GRM: beta1 -0.62 0.46 -1.45 0.19
GRM: beta2 0.23 0.59 -0.78 1.36
GPCM: alpha 0.93 0.15 0.62 1.21
GPCM: delta1 0.16 0.82 -1.30 2.24
GPCM: delta2 -0.08 0.98 -1.41 2.11
GPCM: delta3 0.39 1.08 -1.71 2.36
Response Time 56.17 15.37 22.00 129.00

Example

Top-down 1-2 MST

Assemble 2 panels of 1-2 MST using the top-down design approach. Each route includes 40 items, and no item reuse is allowed. Maximize TIF over the [-1.64, 0] for the route 1M-2E and over [0, 1.64] for the route 1M-2H in hopes of covering the region [-1.64, 1.64] (90% of the population) jointly with adequate test information. To achieve a more balanced routing distribution, anchor the TIF intersection of Module 2E and 2M at 0.

As for non-statistical constraints, each route is required to have 36 3PL items, 2 GPCM items, and 2 GRM items. Also, each route includes 9 to 11 items in each of the four content domains and has an average response time of 60 +/- 4 seconds per item.

### Step 1: Prepare the Item Pool
theta_values<-unique(c(seq(-1.64,0,length.out = 3),
                       seq(0,1.64,length.out = 3)))
theta_iif<-compute_iif(Rmst_pool,
                       item_par_cols = item_par_cols,
                       theta = theta_values,model_col = "model",
                       D = 1)
Rmst_pool[,paste0("iif(theta=",theta_values,")")]<-theta_iif

# check attribute availability
item_cat_values<-vapply(content_categories,FUN = function(cat_level) 
  get_attribute_val(Rmst_pool,attribute = "content",cat_level = cat_level), 
  FUN.VALUE = integer(PoolSize))
# number of items in each category
apply(item_cat_values,2,sum)
#>   1   2   3   4 
#> 127  96 103 114

### Step 2: Specify the MST Structure: 40 items in each pathway, routing decision point is 0.
TD_12 <- mst_design(itempool = Rmst_pool,item_id_col = "Item_id",
                     design = "1-2",rdps = list(0),pathway_length = 40)

### Step 3: Identify hierarchical requirements
# mst structure constraints: pathway length, at least one item per module, 
#                            routing decision point is 0.
# item reusage: no item reuse within a panel, no item reuse across 2 panels.
# num of items from different IRT model per pathway: 36 3PL items, 2 GPCM items, and 2 GRM items
# num of items from each content domain per pathway: 9 to 11 items in each domain.
# average response time: each pathway has 60 +/- 4 seconds per item.
# TIF: three equal interval points in [-1.64, 0] for the route 1M-2E 
#      and in [0, 1.64] for the route 1M-2H

### Step 4: Translate specifications 
TD12_structure<-mst_structure_con(x = TD_12)
TD12_itemtype<-test_itemcat_con(x = TD_12,attribute = "model",
                                cat_levels = c("3PL","GPCM","GRM"),
                                operator = "=",
                                target_num = c(36,2,2),
                                which_pathway = 1:2)
TD12_content<-test_itemcat_range_con(x = TD_12,attribute = "content",
                                     cat_levels = content_categories,
                                     min = 9,max = 11,
                                     which_pathway = 1:2)
TD12_time<-test_itemquant_range_con(x = TD_12,attribute = "time",
                                    min = 56*40,max = 64*40,
                                    which_pathway = 1:2)
TD12_noreuse<-panel_itemreuse_con(x = TD_12,overlap = FALSE)
TD12_obj1<-objective_term(x = TD_12,attribute = "iif(theta=-1.64)",
                          applied_level = "Pathway-level",
                          which_pathway = 1,sense = "max")
TD12_obj2<-objective_term(x = TD_12,attribute = "iif(theta=-0.82)",
                          applied_level = "Pathway-level",
                          which_pathway = 1,sense = "max")
TD12_obj3<-objective_term(x = TD_12,attribute = "iif(theta=0)",
                          applied_level = "Pathway-level",
                          which_pathway = 1,sense = "max")
TD12_obj4<-objective_term(x = TD_12,attribute = "iif(theta=0)",
                          applied_level = "Pathway-level",
                          which_pathway = 2,sense = "max")
TD12_obj5<-objective_term(x = TD_12,attribute = "iif(theta=0.82)",
                          applied_level = "Pathway-level",
                          which_pathway = 2,sense = "max")
TD12_obj6<-objective_term(x = TD_12,attribute = "iif(theta=1.64)",
                          applied_level = "Pathway-level",
                          which_pathway = 2,sense = "max")
TD12_obj<-capped_maximin_obj(x = TD_12,
                             multiple_terms = list(TD12_obj1,TD12_obj2,TD12_obj3,
                                                   TD12_obj4,TD12_obj5,TD12_obj6))
TD12_onepanel<-onepanel_spec(x = TD_12,
                             constraints = list(TD12_structure,TD12_noreuse,
                                                TD12_itemtype,TD12_content,
                                                TD12_time),
                             objective = TD12_obj)
TD12_model<-multipanel_spec(x = TD_12,panel_model = TD12_onepanel,
                            num_panels = 2,
                            global_min_use = 0,global_max_use = 1)                               

# It is not executed in the vignette to avoid long build times.
# \dontrun{
# ### Step 5: Execute assembly via solver
# TD12_result<-solve_model(model_spec = TD12_model,solver = "HiGHS",time_limit = 5*60)
# TD12_panel<-assembled_panel(x = TD_12,result = TD12_result)
# ### Step 6: Diagnose infeasible model
# # There is an optimal solution. Skip this step.
# }
# ### Step 7: Evaluate panel
# Instead, we load a precomputed result:
data("TD12_panel")

# number of items in each content domain
report_test_itemcat(assembled_panel = TD12_panel,
                    attribute = "content",
                    cat_levels = content_categories,
                    which_pathway = 1:2)
#>    panel_id pathway_id  1  2 3  4
#> 1   Panel_1        M-E 11  9 9 11
#> 2   Panel_1        M-H 11 11 9  9
#> 11  Panel_2        M-E 11 10 9 10
#> 21  Panel_2        M-H 11  9 9 11
# number of items in each model
report_test_itemcat(assembled_panel = TD12_panel,
                    attribute = "model",
                    cat_levels = c("3PL","GPCM","GRM"),
                    which_pathway = 1:2)
#>    panel_id pathway_id 3PL GPCM GRM
#> 1   Panel_1        M-E  36    2   2
#> 2   Panel_1        M-H  36    2   2
#> 11  Panel_2        M-E  36    2   2
#> 21  Panel_2        M-H  36    2   2
# average time in each pathway per item
report_test_itemquant(assembled_panel = TD12_panel,
                      attribute = "time",
                      statistic = "average",
                      which_pathway = 1:2)
#>   panel_id pathway_id attribute average
#> 1  Panel_1        M-E      time  56.350
#> 2  Panel_1        M-H      time  57.650
#> 3  Panel_2        M-E      time  56.025
#> 4  Panel_2        M-H      time  57.750
# TIF at [-1.64.0] for the route 1M-2E
report_test_tif(assembled_panel = TD12_panel,
                theta = seq(-1.64,0,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",
                which_pathway = 1)
#>   panel_id theta information pathway_id
#> 1  Panel_1 -1.64    7.517750        M-E
#> 2  Panel_1 -0.82    9.200638        M-E
#> 3  Panel_1  0.00    7.521433        M-E
#> 4  Panel_2 -1.64    7.533666        M-E
#> 5  Panel_2 -0.82    9.232393        M-E
#> 6  Panel_2  0.00    7.540688        M-E
# TIF at [0,1.64] for the route 1M-2H
report_test_tif(assembled_panel = TD12_panel,
                theta = seq(0,1.64,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",
                which_pathway = 2)
#>   panel_id theta information pathway_id
#> 1  Panel_1  0.00    7.601128        M-H
#> 2  Panel_1  0.82    9.169080        M-H
#> 3  Panel_1  1.64    7.560521        M-H
#> 4  Panel_2  0.00    7.537336        M-H
#> 5  Panel_2  0.82    9.221848        M-H
#> 6  Panel_2  1.64    7.711617        M-H
# TIF plots
plot_panel_tif(assembled_panel = TD12_panel,
               item_par_cols = item_par_cols,
               model_col = "model",
               theta = seq(-3,3,0.1),
               unit = "pathway",mode = "within_panel")

# analytic evaluation
TD12_precision<-analytic_mst_precision(design = "1-2",assembled_panel = TD12_panel,
                                       item_par_cols = item_par_cols,
                                       model_col = "model",
                                       theta = seq(-3,3,0.1),
                                       range_tcc = c(-5,5),
                                       rdps  = list(0))
TD12_bias_CSEM_panel1<-TD12_precision$Panel_1$eval_tb
TD12_bias_CSEM_panel2<-TD12_precision$Panel_2$eval_tb
TD12_bias_CSEM_panel1[,"Panel"]<-"Panel_1"
TD12_bias_CSEM_panel2[,"Panel"]<-"Panel_2"
TD12_bias_CSEM<-rbind(TD12_bias_CSEM_panel1,TD12_bias_CSEM_panel2)
TD12_bias_CSEM$Panel<-as.factor(TD12_bias_CSEM$Panel)
ggplot(TD12_bias_CSEM, aes(x = theta, y = bias, color = Panel)) +
  geom_line() +
  geom_point(size = 1.5) +
  labs(
    x = expression(theta),
    y = "Bias",
    color = "Panel"
  ) +
  theme_bw()

ggplot(TD12_bias_CSEM, aes(x = theta, y = csem, color = Panel)) +
  geom_line() +
  geom_point(size = 1.5) +
  labs(
    x = expression(theta),
    y = "CSEM",
    color = "Panel"
  ) +
  theme_bw()

Bottom-up 1-3 MST

Assemble 2 panels of 1-3 MST using the bottom-up design approach. Each module includes 20 items, and no item reuse is allowed. Maximize TIF over the [-1.96, -0.65] for the easy module, over [-0.65, 0.65] for the moderate modules, and over [0.65, 1.96] for the hard module. In addition, each module is required to comply with the following constraints: 15 3PL items, 2 to 3 GPCM items, and 2 to 3 GRM items; 4 to 6 items in each of the four content domains; 56 to 64 seconds of response time per item

### Step 1: Prepare the Item Pool
theta_values<-unique(c(seq(-0.65,0.65,length.out = 3),
                       seq(-1.96,-0.65,length.out = 3),
                       seq(0.65,1.96,length.out = 3)))
theta_iif<-compute_iif(Rmst_pool,
                       item_par_cols = item_par_cols,
                       theta = theta_values,model_col = "model",
                       D = 1)
Rmst_pool[,paste0("iif(theta=",theta_values,")")]<-theta_iif

### Step 2: Specify the MST Structure: 20 items in each model. no RDP specified
BU_13 <- mst_design(itempool = Rmst_pool,item_id_col = "Item_id",
                    design = "1-3",rdps = NULL,module_length = c(20,20,20,20))

### Step 3: Identify hierarchical requirements
# mst structure constraints: module length
# item reusage: no item reuse within a panel, no item reuse across 2 panels.
# TIF: three equal interval points in [-1.96, -0.65] for the 2E module, 
#      in [-0.65, 0.65] for 1M and 2M modules, 
#      and in [0.65, 1.96] for the 2H module.
# number of items from different IRT model:
# each module has 15 3PL items, 2 to 3 GPCM items, and 2 to 3 GRM items
# number of items from each content domain: 
# each module includes 4 to 6 items in each domain.
# average response time: each module has 60 +/- 4 seconds per item.

### Step 4: Translate specifications 
BU13_structure<-mst_structure_con(x = BU_13)
#> 'ModuleLength' present in design; ignoring stage-level bounds for length.
BU13_obj1<-objective_term(x = BU_13,attribute = "iif(theta=-0.65)",
                          applied_level = "Module-level",
                          which_module = 1,sense = "max")
BU13_obj2<-objective_term(x = BU_13,attribute = "iif(theta=0)",
                          applied_level = "Module-level",
                          which_module = 1,sense = "max")
BU13_obj3<-objective_term(x = BU_13,attribute = "iif(theta=0.65)",
                          applied_level = "Module-level",
                          which_module = 1,sense = "max")
BU13_obj4<-objective_term(x = BU_13,attribute = "iif(theta=-1.96)",
                          applied_level = "Module-level",
                          which_module = 2,sense = "max")
BU13_obj5<-objective_term(x = BU_13,attribute = "iif(theta=-1.305)",
                          applied_level = "Module-level",
                          which_module = 2,sense = "max")
BU13_obj6<-objective_term(x = BU_13,attribute = "iif(theta=-0.65)",
                          applied_level = "Module-level",
                          which_module = 2,sense = "max")
BU13_obj7<-objective_term(x = BU_13,attribute = "iif(theta=-0.65)",
                          applied_level = "Module-level",
                          which_module = 3,sense = "max")
BU13_obj8<-objective_term(x = BU_13,attribute = "iif(theta=0)",
                          applied_level = "Module-level",
                          which_module = 3,sense = "max")
BU13_obj9<-objective_term(x = BU_13,attribute = "iif(theta=0.65)",
                          applied_level = "Module-level",
                          which_module = 3,sense = "max")
BU13_obj10<-objective_term(x = BU_13,attribute = "iif(theta=0.65)",
                           applied_level = "Module-level",
                           which_module = 4,sense = "max")
BU13_obj11<-objective_term(x = BU_13,attribute = "iif(theta=1.305)",
                           applied_level = "Module-level",
                           which_module = 4,sense = "max")
BU13_obj12<-objective_term(x = BU_13,attribute = "iif(theta=1.96)",
                           applied_level = "Module-level",
                           which_module = 4,sense = "max")
BU13_obj<-capped_maximin_obj(x = BU_13,
                             multiple_terms = list(BU13_obj1,BU13_obj2,BU13_obj3,
                                                   BU13_obj4,BU13_obj5,BU13_obj6,
                                                   BU13_obj7,BU13_obj8,BU13_obj9,
                                                   BU13_obj10,BU13_obj11,BU13_obj12))
BU13_itemtype<-test_itemcat_range_con(x = BU_13,attribute = "model",
                                      cat_levels = c("3PL","GPCM","GRM"),
                                      min = c(15,2,2),max = c(15,3,3),
                                      which_module = 1:4)
BU13_content<-test_itemcat_range_con(x = BU_13,attribute = "content",
                                     cat_levels = content_categories,
                                     min = 4,max = 6,
                                     which_module = 1:4)
BU13_time<-test_itemquant_range_con(x = BU_13,attribute = "time",
                                    min = 56*20,max = 64*20,
                                    which_module = 1:4)
BU13_noreuse<-panel_itemreuse_con(x = BU_13,overlap = FALSE)
BU13_onepanel<-onepanel_spec(x = BU_13,
                             constraints = list(BU13_structure,BU13_noreuse,
                                                BU13_itemtype,BU13_content,BU13_time),
                             objective = BU13_obj)
BU13_model<-multipanel_spec(x = BU_13,panel_model = BU13_onepanel,
                            num_panels = 2,
                            global_min_use = 0,global_max_use = 1)  
# \dontrun{
# ### Step 5: Execute assembly via solver
# BU13_result<-solve_model(model_spec = BU13_model,solver = "HiGHS",time_limit = 5*60)
# BU13_panel<-assembled_panel(x = BU_13,result = BU13_result)
# ### Step 6: Diagnose infeasible model
# There is an optimal solution. Skip this step.
# }
data("BU13_panel")
### Step 7: Evaluate panel
report_test_itemcat(assembled_panel = BU13_panel,
                    attribute = "content",
                    cat_levels = content_categories,
                    which_module = 1:4)
#>    panel_id module_id 1 2 3 4
#> 1   Panel_1       S1M 4 6 5 5
#> 2   Panel_1       S2E 5 5 5 5
#> 3   Panel_1       S2M 6 4 6 4
#> 4   Panel_1       S2H 6 4 5 5
#> 11  Panel_2       S1M 4 6 5 5
#> 21  Panel_2       S2E 4 6 4 6
#> 31  Panel_2       S2M 4 6 4 6
#> 41  Panel_2       S2H 5 5 4 6
report_test_itemcat(assembled_panel = BU13_panel,
                    attribute = "model",
                    cat_levels = c("3PL","GPCM","GRM"),
                    which_module = 1:4)
#>    panel_id module_id 3PL GPCM GRM
#> 1   Panel_1       S1M  15    2   3
#> 2   Panel_1       S2E  15    2   3
#> 3   Panel_1       S2M  15    3   2
#> 4   Panel_1       S2H  15    3   2
#> 11  Panel_2       S1M  15    2   3
#> 21  Panel_2       S2E  15    2   3
#> 31  Panel_2       S2M  15    3   2
#> 41  Panel_2       S2H  15    3   2
# average time in each module per item
report_test_itemquant(assembled_panel = BU13_panel,
                      attribute = "time",
                      statistic = "average",
                      which_module = 1:4)
#>   panel_id module_id attribute average
#> 1  Panel_1       S1M      time   56.70
#> 2  Panel_1       S2E      time   56.00
#> 3  Panel_1       S2M      time   56.05
#> 4  Panel_1       S2H      time   57.75
#> 5  Panel_2       S1M      time   56.05
#> 6  Panel_2       S2E      time   56.45
#> 7  Panel_2       S2M      time   56.00
#> 8  Panel_2       S2H      time   56.10
# TIF at [-0.65, 0.65] for 1M and 2M modules
report_test_tif(assembled_panel = BU13_panel,
                theta = seq(-0.65,0.65,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",which_module = c(1,3))
#>    panel_id theta information module_id
#> 1   Panel_1 -0.65    3.922799       S1M
#> 2   Panel_1  0.00    4.842350       S1M
#> 3   Panel_1  0.65    4.795111       S1M
#> 4   Panel_1 -0.65    4.224004       S2M
#> 5   Panel_1  0.00    4.884430       S2M
#> 6   Panel_1  0.65    4.114863       S2M
#> 7   Panel_2 -0.65    3.931018       S1M
#> 8   Panel_2  0.00    4.886804       S1M
#> 9   Panel_2  0.65    3.896316       S1M
#> 10  Panel_2 -0.65    4.154348       S2M
#> 11  Panel_2  0.00    4.887273       S2M
#> 12  Panel_2  0.65    3.999800       S2M
# TIF at [-1.96, -0.65] for the 2E module
report_test_tif(assembled_panel = BU13_panel,
                theta = seq(-1.96,-0.65,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",which_module = 2)
#>   panel_id  theta information module_id
#> 1  Panel_1 -1.960    3.897463       S2E
#> 2  Panel_1 -1.305    4.886115       S2E
#> 3  Panel_1 -0.650    4.899734       S2E
#> 4  Panel_2 -1.960    3.903641       S2E
#> 5  Panel_2 -1.305    4.900072       S2E
#> 6  Panel_2 -0.650    4.911936       S2E
# TIF at [0.65, 1.96] for the 2H module
report_test_tif(assembled_panel = BU13_panel,
                theta = seq(0.65,1.96,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",which_module = 4)
#>   panel_id theta information module_id
#> 1  Panel_1 0.650    4.894815       S2H
#> 2  Panel_1 1.305    4.581293       S2H
#> 3  Panel_1 1.960    3.922658       S2H
#> 4  Panel_2 0.650    4.863998       S2H
#> 5  Panel_2 1.305    4.868411       S2H
#> 6  Panel_2 1.960    3.901820       S2H

plot_panel_tif(assembled_panel = BU13_panel,
               item_par_cols = item_par_cols,
               model_col = "model",theta = seq(-3,3,0.1),unit = "module",mode = "within_panel")

Top-down 1-2-3 MST with reused items

Assemble 2 panels of 1-2-3 MST using the top-down approach. Two routes with capricious ability change (1M-2E-3H & 1M-2H-3E) are blocked. Items are allowed to be used up to four times. Each route needs to meet the following criteria:

Maximize TIF over [-1.96, -0.64] for the easy route (1M-2E-3E), over [-0.64, 0.64] for the moderate routes (1M-2E-3M & 1M-2H-3M), and over [0.64, 1.96] for the hard route (1M-2H-3H) Include a total of 40 items: 34 to 38 3PL items, 1 to 3 GPCM items, and 1 to 3 GRM items. 8 to 12 items in each of the four content domains. 56 to 64 seconds of response time per item. To yield decent routing accuracy, the first two stages are required to include at least 8 items.

### Step 1: Prepare the Item Pool
theta_values<-unique(c(seq(-0.64,0.64,length.out = 3),
                       seq(-1.96,-0.64,length.out = 3),
                       seq(0.64,1.96,length.out = 3)))
theta_iif<-compute_iif(Rmst_pool,
                       item_par_cols = item_par_cols,
                       theta = theta_values,model_col = "model",
                       D = 1)
Rmst_pool[,paste0("iif(theta=",theta_values,")")]<-theta_iif

### Step 2: Specify the MST Structure: 40 items in each pathway, routing decision point is 0.
TD_123 <- mst_design(itempool = Rmst_pool,item_id_col = "Item_id",
                     design = "1-2-3",rdps = NULL,exclude_pathways = c("1-1-3","1-2-1"),
                     pathway_length = 40)

### Step 3: Identify hierarchical requirements
# mst structure constraints: pathway length, minimum stage length in stage 1 and 2. 
# no 1M-2E-3H & 1M-2H-3E pathways.
# item reusage: no items reused within a pathway, an item can be 
# at most selected four times across 2 panels.
# TIF: three equal interval points in [-1.96, -0.64] for the easy route (1M-2E-3E), 
# over [-0.64, 0.64] for the moderate routes (1M-2E-3M & 1M-2H-3M), 
# and over [0.64, 1.96] for the hard route (1M-2H-3H)
# number of items from different IRT model: each pathway has 34 to 38 3PL items, 
# 1 to 3 GPCM items, and 1 to 3 GRM items. 
# number of items from each content domain: each pathway includes 8 to 12 items in each domain.
# average response time: each pathway has 60 +/- 4 seconds per item.

### Step 4: Translate specifications 
TD123_structure<-mst_structure_con(x = TD_123,
                                   stage_length_bound = data.frame(stage = c(1,2,3),
                                                                   min = c(8,8,NA_integer_),
                                                                   max = rep(NA_integer_,3)))
TD123_obj1<-objective_term(x = TD_123,attribute = "iif(theta=-1.96)",
                           applied_level = "Pathway-level",
                           which_pathway = 1,sense = "max")
TD123_obj2<-objective_term(x = TD_123,attribute = "iif(theta=-1.3)",
                           applied_level = "Pathway-level",
                           which_pathway = 1,sense = "max")
TD123_obj3<-objective_term(x = TD_123,attribute = "iif(theta=-0.64)",
                           applied_level = "Pathway-level",
                           which_pathway = 1,sense = "max")
TD123_obj4<-objective_term(x = TD_123,attribute = "iif(theta=-0.64)",
                           applied_level = "Pathway-level",
                           which_pathway = 2,sense = "max")
TD123_obj5<-objective_term(x = TD_123,attribute = "iif(theta=0)",
                           applied_level = "Pathway-level",
                           which_pathway = 2,sense = "max")
TD123_obj6<-objective_term(x = TD_123,attribute = "iif(theta=0.64)",
                           applied_level = "Pathway-level",
                           which_pathway = 2,sense = "max")
TD123_obj7<-objective_term(x = TD_123,attribute = "iif(theta=-0.64)",
                           applied_level = "Pathway-level",
                           which_pathway = 3,sense = "max")
TD123_obj8<-objective_term(x = TD_123,attribute = "iif(theta=0)",
                           applied_level = "Pathway-level",
                           which_pathway = 3,sense = "max")
TD123_obj9<-objective_term(x = TD_123,attribute = "iif(theta=0.64)",
                           applied_level = "Pathway-level",
                           which_pathway = 3,sense = "max")
TD123_obj10<-objective_term(x = TD_123,attribute = "iif(theta=0.64)",
                            applied_level = "Pathway-level",
                            which_pathway = 4,sense = "max")
TD123_obj11<-objective_term(x = TD_123,attribute = "iif(theta=1.3)",
                            applied_level = "Pathway-level",
                            which_pathway = 4,sense = "max")
TD123_obj12<-objective_term(x = TD_123,attribute = "iif(theta=1.96)",
                            applied_level = "Pathway-level",
                            which_pathway = 4,sense = "max")
TD123_obj<-capped_maximin_obj(x = TD_123,
                              multiple_terms = list(TD123_obj1,TD123_obj2,TD123_obj3,
                                                    TD123_obj4,TD123_obj5,TD123_obj6,
                                                    TD123_obj7,TD123_obj8,TD123_obj9,
                                                    TD123_obj10,TD123_obj11,TD123_obj12))
TD123_itemtype<-test_itemcat_range_con(x = TD_123,attribute = "model",
                                       cat_levels = c("3PL","GPCM","GRM"),
                                       min = c(34,1,1),max = c(38,3,3),
                                       which_pathway = 1:4)
TD123_content<-test_itemcat_range_con(x = TD_123,attribute = "content",
                                      cat_levels = content_categories,
                                      min = 8,max = 12,
                                      which_pathway = 1:4)
TD123_time<-test_itemquant_range_con(x = TD_123,attribute = "time",
                                     min = 56*40,max = 64*40,
                                     which_pathway = 1:4)
TD123_noreuse<-panel_itemreuse_con(x = TD_123,overlap = TRUE)
TD123_onepanel<-onepanel_spec(x = TD_123,
                              constraints = list(TD123_structure,TD123_noreuse,
                                                 TD123_itemtype,TD123_content,TD123_time),
                              objective = TD123_obj)
TD123_model<-multipanel_spec(x = TD_123,panel_model = TD123_onepanel,
                             num_panels = 2,global_max_use = 4)

# \dontrun{
# ### Step 5: Execute assembly via solver
# TD123_result<-solve_model(model_spec = TD123_model,solver = "HiGHS",time_limit = 5*60)
# TD123_panel<-assembled_panel(x = TD_123,result = TD123_result)
# ### Step 6: Diagnose infeasible model
# There is an optimal solution. Skip this step.
# }
data("TD123_panel")
### Step 7: Evaluate panel
report_test_itemcat(assembled_panel = TD123_panel,
                    attribute = "content",
                    cat_levels = content_categories,
                    which_pathway = 1:4)
#>    panel_id pathway_id  1  2  3  4
#> 1   Panel_1      M-E-E 12  9  8 11
#> 2   Panel_1      M-E-M 12  8  8 12
#> 3   Panel_1      M-H-M  9 11  8 12
#> 4   Panel_1      M-H-H 11  9 11  9
#> 11  Panel_2      M-E-E 10 12  8 10
#> 21  Panel_2      M-E-M 12  8  8 12
#> 31  Panel_2      M-H-M  9 11  8 12
#> 41  Panel_2      M-H-H 11  9 11  9
report_test_itemcat(assembled_panel = TD123_panel,
                    attribute = "model",
                    cat_levels = c("3PL","GPCM","GRM"),
                    which_pathway = 1:4)
#>    panel_id pathway_id 3PL GPCM GRM
#> 1   Panel_1      M-E-E  36    1   3
#> 2   Panel_1      M-E-M  36    1   3
#> 3   Panel_1      M-H-M  36    1   3
#> 4   Panel_1      M-H-H  34    3   3
#> 11  Panel_2      M-E-E  35    2   3
#> 21  Panel_2      M-E-M  35    2   3
#> 31  Panel_2      M-H-M  36    1   3
#> 41  Panel_2      M-H-H  34    3   3
# average time in each module per item
report_test_itemquant(assembled_panel = TD123_panel,
                      attribute = "time",
                      statistic = "average",
                      which_pathway = 1:4)
#>   panel_id pathway_id attribute average
#> 1  Panel_1      M-E-E      time  56.375
#> 2  Panel_1      M-E-M      time  56.425
#> 3  Panel_1      M-H-M      time  56.250
#> 4  Panel_1      M-H-H      time  56.100
#> 5  Panel_2      M-E-E      time  56.100
#> 6  Panel_2      M-E-M      time  56.150
#> 7  Panel_2      M-H-M      time  56.225
#> 8  Panel_2      M-H-H      time  56.100
# TIF at [-1.96, -0.64] for the easy route (1M-2E-3E)
report_test_tif(assembled_panel = TD123_panel,
                theta = seq(-1.96,-0.64,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",which_pathway = 1)
#>   panel_id theta information pathway_id
#> 1  Panel_1 -1.96    5.639209      M-E-E
#> 2  Panel_1 -1.30    6.856428      M-E-E
#> 3  Panel_1 -0.64    7.113380      M-E-E
#> 4  Panel_2 -1.96    5.756675      M-E-E
#> 5  Panel_2 -1.30    7.070713      M-E-E
#> 6  Panel_2 -0.64    7.450101      M-E-E
# TIF at [-0.64, 0.64] for the moderate routes (1M-2E-3M & 1M-2H-3M),
report_test_tif(assembled_panel = TD123_panel,
                theta = seq(-0.64,0.64,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",which_pathway = 2:3)
#>    panel_id theta information pathway_id
#> 1   Panel_1 -0.64    6.128133      M-E-M
#> 2   Panel_1  0.00    5.518658      M-E-M
#> 3   Panel_1  0.64    4.807058      M-E-M
#> 4   Panel_1 -0.64    5.589652      M-H-M
#> 5   Panel_1  0.00    4.967843      M-H-M
#> 6   Panel_1  0.64    4.285901      M-H-M
#> 7   Panel_2 -0.64    6.519357      M-E-M
#> 8   Panel_2  0.00    6.487888      M-E-M
#> 9   Panel_2  0.64    5.959664      M-E-M
#> 10  Panel_2 -0.64    5.627857      M-H-M
#> 11  Panel_2  0.00    5.057230      M-H-M
#> 12  Panel_2  0.64    4.421342      M-H-M
# TIF at [0.64, 1.96] for the hard route (1M-2H-3H)
report_test_tif(assembled_panel = TD123_panel,
                theta = seq(0.64,1.96,length.out = 3),
                item_par_cols = item_par_cols,
                model_col = "model",which_pathway = 4)
#>   panel_id theta information pathway_id
#> 1  Panel_1  0.64    7.693210      M-H-H
#> 2  Panel_1  1.30    7.202137      M-H-H
#> 3  Panel_1  1.96    6.231615      M-H-H
#> 4  Panel_2  0.64    7.693210      M-H-H
#> 5  Panel_2  1.30    7.202137      M-H-H
#> 6  Panel_2  1.96    6.231615      M-H-H


plot_panel_tif(assembled_panel = TD123_panel,
               item_par_cols = item_par_cols,
               model_col = "model",theta = seq(-3,3,0.1),
               unit = "pathway",mode = "within_panel")

Hybrid 1-3 MST

In the Rmst package demonstration of the hybrid assembly for an MST 1–3 design, the example appears to use an item pool generated with a seed different from the default setting. When running the same demonstration code with the default seed, the solver returns “No solution for the LP model.” Therefore, the Hybrid 1–3 panel was assembled using specifications that differ from those in the Rmst package demonstration.

Assemble 2 panels of 1-3 MST using the extended-hybrid assembly approach. Each module includes 20 items, and no item reuse is allowed. Maximize TIF over the [-1.96, -0.65] for the easy pathways, over [-0.65, 0.65] for the moderate pathways, and over [0.65, 1.96] for the hard pathways. In addition, each module is required to comply with the following constraints: 15 3PL items, 2 to 3 GPCM items, and 2 to 3 GRM items; 4 to 6 items in each of the four content domains; each pathway is required to have 56 to 64 seconds of response time per item

### Step 1: Prepare the Item Pool
theta_values<-unique(c(seq(-0.65,0.65,length.out = 3),
                       seq(-1.96,-0.65,length.out = 3),
                       seq(0.65,1.96,length.out = 3)))
theta_iif<-compute_iif(Rmst_pool,
                       item_par_cols = item_par_cols,
                       theta = theta_values,model_col = "model",
                       D = 1)
Rmst_pool[,paste0("iif(theta=",theta_values,")")]<-theta_iif

### Step 2: Specify the MST Structure: 20 items in each pathway
Hy_13 <- mst_design(itempool = Rmst_pool,item_id_col = "Item_id",
                     design = "1-3",rdps = NULL,
                     module_length = c(20,20,20,20))

### Step 3: Identify hierarchical requirements
# mst structure constraints: module length
# item reusage: no item reuse within a panel, no item reuse across 2 panels.
# TIF: minimum TIF of 6 at three equal interval points in [-1.28, 1.28] for 
# routing module, minimum TIF of 8 at -1.28 for S2E, 0 for S2M, 1.28 for S2H modules
# number of items from each content domain: each pathway includes 4 to 6 items in each domain.
# TIF: three equal interval points in [-1.96, -0.65] for the S1R-S2E pathways, 
#      in [-0.65, 0.65] for S1R-S2M pathways, 
#      and in [0.65, 1.96] for S1R-S2H pathways.
# number of items from different IRT model:
# each module has 15 3PL items, 2 to 3 GPCM items, and 2 to 3 GRM items
# number of items from each content domain: 
# each module includes 4 to 6 items in each domain.
# average response time: each pathway has 60 +/- 4 seconds per item.


### Step 4: Translate specifications 
Hy13_structure<-mst_structure_con(x = Hy_13)
#> 'ModuleLength' present in design; ignoring stage-level bounds for length.
Hy13_noreuse<-panel_itemreuse_con(x = Hy_13,overlap = FALSE)
Hy13_itemtype<-test_itemcat_range_con(x = Hy_13,attribute = "model",
                                   cat_levels = c("3PL","GPCM","GRM"),
                                   min = c(15,2,2),max = c(15,3,3),
                                  which_module = 1:4)
Hy13_content<-test_itemcat_range_con(x = Hy_13,attribute = "content",
                                     cat_levels = content_categories,
                                     min = 4,max = 6,
                                     which_module = 1:4)
Hy13_time<-test_itemquant_range_con(x = Hy_13,attribute = "time",
                                    min = 56*40,max = 64*40,
                                    which_pathway = 1:3)
Hy13_obj1<-objective_term(x = Hy_13,attribute = "iif(theta=-0.65)",
                          applied_level = "Pathway-level",
                          which_pathway = 1,sense = "max")
Hy13_obj2<-objective_term(x = Hy_13,attribute = "iif(theta=0)",
                          applied_level = "Pathway-level",
                          which_pathway = 1,sense = "max")
Hy13_obj3<-objective_term(x = Hy_13,attribute = "iif(theta=0.65)",
                          applied_level = "Pathway-level",
                          which_pathway = 1,sense = "max")
Hy13_obj4<-objective_term(x = Hy_13,attribute = "iif(theta=-1.96)",
                          applied_level = "Pathway-level",
                          which_pathway = 2,sense = "max")
Hy13_obj5<-objective_term(x = Hy_13,attribute = "iif(theta=-1.305)",
                          applied_level = "Pathway-level",
                          which_pathway = 2,sense = "max")
Hy13_obj6<-objective_term(x = Hy_13,attribute = "iif(theta=-0.65)",
                          applied_level = "Pathway-level",
                          which_pathway = 2,sense = "max")
Hy13_obj7<-objective_term(x = Hy_13,attribute = "iif(theta=-0.65)",
                          applied_level = "Pathway-level",
                          which_pathway = 3,sense = "max")
Hy13_obj8<-objective_term(x = Hy_13,attribute = "iif(theta=0)",
                          applied_level = "Pathway-level",
                          which_pathway = 3,sense = "max")
Hy13_obj9<-objective_term(x = Hy_13,attribute = "iif(theta=0.65)",
                          applied_level = "Pathway-level",
                          which_pathway = 3,sense = "max")
Hy13_obj<-capped_maximin_obj(x = Hy_13,
                             multiple_terms = list(Hy13_obj1,Hy13_obj2,Hy13_obj3,
                                            Hy13_obj4,Hy13_obj5,Hy13_obj6,
                                            Hy13_obj7,Hy13_obj8,Hy13_obj9))
Hy13_onepanel<-onepanel_spec(x = Hy_13,
                             constraints = list(Hy13_structure,Hy13_noreuse,
                                                Hy13_itemtype,Hy13_content,
                                                Hy13_time),
                             objective = Hy13_obj)
Hy13_model<-multipanel_spec(x = Hy_13,panel_model = Hy13_onepanel,
                            num_panels = 2,global_max_use = 1)

# \dontrun{
# ### Step 5: Execute assembly via solver
# Hy13_result<-solve_model(model_spec = Hy13_model,solver = "HiGHS",time_limit = 5*60)
# Hy13_panel<-assembled_panel(x = Hy_13,result = Hy13_result)
# ### Step 6: Diagnose infeasible model
# There is an optimal solution. Skip this step.
# }
data("Hy13_panel")
### Step 7: Evaluate panel
report_test_itemcat(assembled_panel = Hy13_panel,
                    attribute = "content",
                    cat_levels = content_categories,
                    which_module = 1:4)
#>    panel_id module_id 1 2 3 4
#> 1   Panel_1       S1M 4 6 4 6
#> 2   Panel_1       S2E 6 5 4 5
#> 3   Panel_1       S2M 5 5 5 5
#> 4   Panel_1       S2H 4 5 6 5
#> 11  Panel_2       S1M 4 6 4 6
#> 21  Panel_2       S2E 5 4 5 6
#> 31  Panel_2       S2M 4 5 5 6
#> 41  Panel_2       S2H 6 4 6 4
report_test_itemcat(assembled_panel = Hy13_panel,
                    attribute = "model",
                    cat_levels = c("3PL","GPCM","GRM"),
                    which_module = 1:4)
#>    panel_id module_id 3PL GPCM GRM
#> 1   Panel_1       S1M  15    2   3
#> 2   Panel_1       S2E  15    3   2
#> 3   Panel_1       S2M  15    2   3
#> 4   Panel_1       S2H  15    3   2
#> 11  Panel_2       S1M  15    2   3
#> 21  Panel_2       S2E  15    3   2
#> 31  Panel_2       S2M  15    2   3
#> 41  Panel_2       S2H  15    3   2
report_test_itemquant(assembled_panel = Hy13_panel,
                      attribute = "time",
                      statistic = "average",
                      which_pathway = 1:3)
#>   panel_id pathway_id attribute average
#> 1  Panel_1        M-E      time  56.175
#> 2  Panel_1        M-M      time  56.225
#> 3  Panel_1        M-H      time  56.575
#> 4  Panel_2        M-E      time  56.675
#> 5  Panel_2        M-M      time  56.200
#> 6  Panel_2        M-H      time  56.700
plot_panel_tif(assembled_panel = Hy13_panel,
               item_par_cols = item_par_cols,
               model_col = "model",theta = seq(-3,3,0.1),
               unit = "pathway",
               mode = "within_panel")

Summary

This vignette demonstrates how the mstATA package can be used to assemble multistage testing (MST) panels using three common strategies: bottom-up, top-down, and hybrid assembly. The examples illustrate how the package provides flexibility in constructing linear constraints for specification requirements that may be enforced at different levels of the test design, including the module, pathway, panel, and solution levels. By formulating these requirements within a unified mixed-integer linear programming (MILP) framework, mstATA enables test developers to adapt assembly strategies to different operational needs while maintaining transparent and reproducible specifications.

Note: Additional examples comparing bottom-up, top-down, and hybrid assembly strategies under the same set of specifications (but enforced at different levels) can be found in the author’s PhD dissertation, which will be available through ProQuest in May 2026.