13  OpenMx

The ggsem package provides integration with OpenMx multi-group models. OpenMx uses a different multi-group architecture than lavaan, so I describe how ggsem handles the structure differences.

Multi-Group Model with Component Subsetting

OpenMx organizes multi-group models as containers with separate submodels for each group. ggsem accesses individual group components using the $ operator to extract each submodel.

library(OpenMx)
library(dplyr)

# Use built-in data for reliability
data(HolzingerSwineford1939, package = "lavaan")

# Simple preparation
hs_data <- HolzingerSwineford1939 %>%
  dplyr::select(-c(id, sex, ageyr, agemo, grade)) %>%
  mutate(school = as.factor(school)) %>%
  na.omit()

# Check groups
table(hs_data$school)

Grant-White     Pasteur 
        145         156 
# Model for Pasteur school
# Model for Pasteur school - COMPLETE with means and all variables
pasteur_data <- hs_data[hs_data$school == "Pasteur", ]
cor_matrix <- cor(pasteur_data[, paste0("x", 1:9)])

# Use average correlations for starting values
avg_cor <- mean(cor_matrix[upper.tri(cor_matrix)])

pasteur_model <- mxModel(
  "Pasteur",
  type = "RAM",
  mxData(pasteur_data, type = "raw"),
  manifestVars = paste0("x", 1:9),
  latentVars = c("visual", "textual", "speed"),

  # Add unique labels to force estimation
  mxPath(from = "visual", to = c("x1", "x2", "x3"),
         free = TRUE, values = c(0.5, 0.6, 0.7),
         labels = c("v_x1", "v_x2", "v_x3")),

  mxPath(from = "textual", to = c("x4", "x5", "x6"),
         free = TRUE, values = c(0.5, 0.6, 0.7),
         labels = c("t_x4", "t_x5", "t_x6")),

  mxPath(from = "speed", to = c("x7", "x8", "x9"),
         free = TRUE, values = c(0.5, 0.6, 0.7),
         labels = c("s_x7", "s_x8", "s_x9")),

  # Residual variances with labels
  mxPath(from = paste0("x", 1:9), arrows = 2, free = TRUE,
         values = runif(9, 0.5, 1.5),  # Random starting values
         labels = paste0("e", 1:9)),

  # Factor variances (fixed for identification)
  mxPath(from = c("visual", "textual", "speed"), arrows = 2, free = FALSE, values = 1),

  # Factor covariances with labels
  mxPath(from = "visual", to = c("textual", "speed"), arrows = 2, free = TRUE,
         values = c(0.2, 0.3), labels = c("v_t", "v_s")),
  mxPath(from = "textual", to = "speed", arrows = 2, free = TRUE,
         values = 0.4, labels = "t_s"),
  mxPath(from = "one", to = paste0("x", 1:9), free = TRUE, values = 0)
)

# Model for Grant-White school (same complete structure)
grantwhite_model <- mxModel(
  "GrantWhite",
  type = "RAM",
  mxData(hs_data[hs_data$school == "Grant-White", ], type = "raw"),
  manifestVars = paste0("x", 1:9),
  latentVars = c("visual", "textual", "speed"),

  mxPath(from = "visual", to = c("x1", "x2", "x3"), free = TRUE, values = 0.8),
  mxPath(from = "textual", to = c("x4", "x5", "x6"), free = TRUE, values = 0.8),
  mxPath(from = "speed", to = c("x7", "x8", "x9"), free = TRUE, values = 0.8),
  mxPath(from = paste0("x", 1:9), arrows = 2, free = TRUE, values = 1),
  mxPath(from = c("visual", "textual", "speed"), arrows = 2, free = FALSE, values = 1),
  mxPath(from = "visual", to = c("textual", "speed"), arrows = 2, free = TRUE, values = 0.3),
  mxPath(from = "textual", to = "speed", arrows = 2, free = TRUE, values = 0.3),

  # MEANS - THIS FIXES THE ERROR!
  mxPath(from = "one", to = paste0("x", 1:9), free = TRUE, values = 0)
)

multi_group_model <- mxModel(
  "Complete_CFA",
  pasteur_model,
  grantwhite_model,
  mxFitFunctionMultigroup(c("Pasteur", "GrantWhite"))
)

# Fit the model
multi_group_fit <- mxRun(multi_group_model)
summary(multi_group_fit)
Summary of Complete_CFA 
 
free parameters:
                  name       matrix     row     col  Estimate  Std.Error A
1                 v_x1    Pasteur.A      x1  visual 1.0473306 0.14082277  
2                 v_x2    Pasteur.A      x2  visual 0.4123451 0.12321757 !
3                 v_x3    Pasteur.A      x3  visual 0.5969065 0.11496274  
4                 t_x4    Pasteur.A      x4 textual 0.9456722 0.07956226 !
5                 t_x5    Pasteur.A      x5 textual 1.1190422 0.08866290 !
6                 t_x6    Pasteur.A      x6 textual 0.8274301 0.06799081 !
7                 s_x7    Pasteur.A      x7   speed 0.5913122 0.10752940 !
8                 s_x8    Pasteur.A      x8   speed 0.6650217 0.10307775 !
9                 s_x9    Pasteur.A      x9   speed 0.5451891 0.10426617  
10                  e1    Pasteur.S      x1      x1 0.2983279 0.25366205 !
11                  e2    Pasteur.S      x2      x2 1.3337155 0.16414995  
12                  e3    Pasteur.S      x3      x3 0.9894968 0.14393858 !
13                  e4    Pasteur.S      x4      x4 0.4253794 0.07039896 !
14                  e5    Pasteur.S      x5      x5 0.4556367 0.08545157  
15                  e6    Pasteur.S      x6      x6 0.2898088 0.05137146  
16                  e7    Pasteur.S      x7      x7 0.8202778 0.12600425  
17                  e8    Pasteur.S      x8      x8 0.5097335 0.11764184  
18                  e9    Pasteur.S      x9      x9 0.6803215 0.11199162  
19                 v_t    Pasteur.S  visual textual 0.4837461 0.07971708  
20                 v_s    Pasteur.S  visual   speed 0.2989546 0.12073169 !
21                 t_s    Pasteur.S textual   speed 0.3251483 0.10214733  
22      Pasteur.M[1,1]    Pasteur.M       1      x1 4.9412392 0.09457150  
23      Pasteur.M[1,2]    Pasteur.M       1      x2 5.9839713 0.09818034  
24      Pasteur.M[1,3]    Pasteur.M       1      x3 2.4871792 0.09288102  
25      Pasteur.M[1,4]    Pasteur.M       1      x4 2.8226464 0.09197512  
26      Pasteur.M[1,5]    Pasteur.M       1      x5 3.9951859 0.10463258  
27      Pasteur.M[1,6]    Pasteur.M       1      x6 1.9221595 0.07903442  
28      Pasteur.M[1,7]    Pasteur.M       1      x7 4.4322730 0.08659981  
29      Pasteur.M[1,8]    Pasteur.M       1      x8 5.5631387 0.07811836  
30      Pasteur.M[1,9]    Pasteur.M       1      x9 5.4177362 0.07916033  
31  GrantWhite.A[1,10] GrantWhite.A      x1  visual 0.7770158 0.10597563 !
32  GrantWhite.A[2,10] GrantWhite.A      x2  visual 0.5719988 0.10263916  
33  GrantWhite.A[3,10] GrantWhite.A      x3  visual 0.7185762 0.09610277  
34  GrantWhite.A[4,11] GrantWhite.A      x4 textual 0.9705183 0.07857795  
35  GrantWhite.A[5,11] GrantWhite.A      x5 textual 0.9606166 0.08265006 !
36  GrantWhite.A[6,11] GrantWhite.A      x6 textual 0.9349374 0.08083198 !
37  GrantWhite.A[7,12] GrantWhite.A      x7   speed 0.6791840 0.08927227 !
38  GrantWhite.A[8,12] GrantWhite.A      x8   speed 0.8325711 0.09477631 !
39  GrantWhite.A[9,12] GrantWhite.A      x9   speed 0.7185135 0.09689711  
40   GrantWhite.S[1,1] GrantWhite.S      x1      x1 0.7148982 0.13129668 !
41   GrantWhite.S[2,2] GrantWhite.S      x2      x2 0.8991978 0.12388520 !
42   GrantWhite.S[3,3] GrantWhite.S      x3      x3 0.5570127 0.10836787  
43   GrantWhite.S[4,4] GrantWhite.S      x4      x4 0.3153072 0.06484995  
44   GrantWhite.S[5,5] GrantWhite.S      x5      x5 0.4188884 0.07229697  
45   GrantWhite.S[6,6] GrantWhite.S      x6      x6 0.4060300 0.06920826  
46   GrantWhite.S[7,7] GrantWhite.S      x7      x7 0.6004932 0.09540502 !
47   GrantWhite.S[8,8] GrantWhite.S      x8      x8 0.4011935 0.11325027  
48   GrantWhite.S[9,9] GrantWhite.S      x9      x9 0.5347789 0.10974399 !
49 GrantWhite.S[10,11] GrantWhite.S  visual textual 0.5406735 0.08524190 !
50 GrantWhite.S[10,12] GrantWhite.S  visual   speed 0.5233423 0.10913170 !
51 GrantWhite.S[11,12] GrantWhite.S textual   speed 0.3361322 0.10066711  
52   GrantWhite.M[1,1] GrantWhite.M       1      x1 4.9298862 0.09536313  
53   GrantWhite.M[1,2] GrantWhite.M       1      x2 6.1999997 0.09196624  
54   GrantWhite.M[1,3] GrantWhite.M       1      x3 1.9956905 0.08603784  
55   GrantWhite.M[1,4] GrantWhite.M       1      x4 3.3172437 0.09311509  
56   GrantWhite.M[1,5] GrantWhite.M       1      x5 4.7120705 0.09619203  
57   GrantWhite.M[1,6] GrantWhite.M       1      x6 2.4689667 0.09396029  
58   GrantWhite.M[1,7] GrantWhite.M       1      x7 3.9208390 0.08557240  
59   GrantWhite.M[1,8] GrantWhite.M       1      x8 5.4882750 0.08687546  
60   GrantWhite.M[1,9] GrantWhite.M       1      x9 5.3272043 0.08513840  

Model Statistics: 
               |  Parameters  |  Degrees of Freedom  |  Fit (-2lnL units)
       Model:             60                   2649              7364.395
   Saturated:             NA                     NA                    NA
Independence:             NA                     NA                    NA
Number of observations/statistics: 301/2709

Information Criteria: 
      |  df Penalty  |  Parameters Penalty  |  Sample-Size Adjusted
AIC:       2066.395               7484.395                 7514.895
BIC:      -7753.740               7706.822                 7516.536
CFI: NA 
TLI: 1   (also known as NNFI) 
RMSEA:  0  [95% CI (NA, NA)]
Prob(RMSEA <= 0.05): NA
To get additional fit indices, see help(mxRefModels)
timestamp: 2026-06-06 09:58:06 
Wall clock time: 0.5107851 secs 
optimizer:  SLSQP 
OpenMx version number: 2.21.13 
Need help?  See help(mxSummary) 
class(multi_group_fit) # ggsem does not work with MxModel class
[1] "MxModel"
attr(,"package")
[1] "OpenMx"
class(multi_group_fit$Pasteur) # but ggsem works with MxRAMModel class, $ is used to "subset" a part of the multi-group model
[1] "MxRAMModel"
attr(,"package")
[1] "OpenMx"

Implementation: Access individual group models from the multi-group container and add them separately to ggsem_builder().

# Multi-group visualization with component subsetting
ggsem_builder() |>
  add_group('Pasteur', object = multi_group_fit$Pasteur, x = -30) |>
  add_group('GrantWhite', object = multi_group_fit$GrantWhite, x = 30) |>
  launch()

Note: Unlike lavaan’s integrated multi-group handling, OpenMx requires explicit component extraction, so when using ggsem_builder(), make sure to subset each group’s model using $.