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.0473319 0.14085107  
2                 v_x2    Pasteur.A      x2  visual 0.4123632 0.12321559 !
3                 v_x3    Pasteur.A      x3  visual 0.5969057 0.11496650  
4                 t_x4    Pasteur.A      x4 textual 0.9456731 0.07956160 !
5                 t_x5    Pasteur.A      x5 textual 1.1190547 0.08866147 !
6                 t_x6    Pasteur.A      x6 textual 0.8274281 0.06798925  
7                 s_x7    Pasteur.A      x7   speed 0.5913037 0.10753482 !
8                 s_x8    Pasteur.A      x8   speed 0.6650313 0.10307367 !
9                 s_x9    Pasteur.A      x9   speed 0.5451721 0.10426747 !
10                  e1    Pasteur.S      x1      x1 0.2983317 0.25370733 !
11                  e2    Pasteur.S      x2      x2 1.3337173 0.16415808 !
12                  e3    Pasteur.S      x3      x3 0.9894962 0.14395350 !
13                  e4    Pasteur.S      x4      x4 0.4253809 0.07039844  
14                  e5    Pasteur.S      x5      x5 0.4556373 0.08545018  
15                  e6    Pasteur.S      x6      x6 0.2898093 0.05137090  
16                  e7    Pasteur.S      x7      x7 0.8202722 0.12600414 !
17                  e8    Pasteur.S      x8      x8 0.5097300 0.11763868 !
18                  e9    Pasteur.S      x9      x9 0.6803351 0.11199165 !
19                 v_t    Pasteur.S  visual textual 0.4837532 0.07971471 !
20                 v_s    Pasteur.S  visual   speed 0.2989447 0.12073681  
21                 t_s    Pasteur.S textual   speed 0.3251456 0.10214734 !
22      Pasteur.M[1,1]    Pasteur.M       1      x1 4.9412379 0.09457229  
23      Pasteur.M[1,2]    Pasteur.M       1      x2 5.9839752 0.09818100  
24      Pasteur.M[1,3]    Pasteur.M       1      x3 2.4871784 0.09288113  
25      Pasteur.M[1,4]    Pasteur.M       1      x4 2.8226455 0.09197584  
26      Pasteur.M[1,5]    Pasteur.M       1      x5 3.9951904 0.10463424  
27      Pasteur.M[1,6]    Pasteur.M       1      x6 1.9221598 0.07903488  
28      Pasteur.M[1,7]    Pasteur.M       1      x7 4.4322749 0.08659940  
29      Pasteur.M[1,8]    Pasteur.M       1      x8 5.5631398 0.07811891  
30      Pasteur.M[1,9]    Pasteur.M       1      x9 5.4177318 0.07916027  
31  GrantWhite.A[1,10] GrantWhite.A      x1  visual 0.7770169 0.10597881 !
32  GrantWhite.A[2,10] GrantWhite.A      x2  visual 0.5720096 0.10263907 !
33  GrantWhite.A[3,10] GrantWhite.A      x3  visual 0.7185842 0.09610335 !
34  GrantWhite.A[4,11] GrantWhite.A      x4 textual 0.9705169 0.07857843  
35  GrantWhite.A[5,11] GrantWhite.A      x5 textual 0.9606061 0.08265029 !
36  GrantWhite.A[6,11] GrantWhite.A      x6 textual 0.9349384 0.08083215  
37  GrantWhite.A[7,12] GrantWhite.A      x7   speed 0.6791992 0.08927281  
38  GrantWhite.A[8,12] GrantWhite.A      x8   speed 0.8325845 0.09477129 !
39  GrantWhite.A[9,12] GrantWhite.A      x9   speed 0.7185246 0.09689750 !
40   GrantWhite.S[1,1] GrantWhite.S      x1      x1 0.7149071 0.13130039 !
41   GrantWhite.S[2,2] GrantWhite.S      x2      x2 0.8991904 0.12388727  
42   GrantWhite.S[3,3] GrantWhite.S      x3      x3 0.5570121 0.10837055 !
43   GrantWhite.S[4,4] GrantWhite.S      x4      x4 0.3153004 0.06484887 !
44   GrantWhite.S[5,5] GrantWhite.S      x5      x5 0.4188901 0.07229589  
45   GrantWhite.S[6,6] GrantWhite.S      x6      x6 0.4060291 0.06920727  
46   GrantWhite.S[7,7] GrantWhite.S      x7      x7 0.6004922 0.09540386 !
47   GrantWhite.S[8,8] GrantWhite.S      x8      x8 0.4012030 0.11324363 !
48   GrantWhite.S[9,9] GrantWhite.S      x9      x9 0.5347829 0.10973482 !
49 GrantWhite.S[10,11] GrantWhite.S  visual textual 0.5406658 0.08524439 !
50 GrantWhite.S[10,12] GrantWhite.S  visual   speed 0.5233550 0.10913246 !
51 GrantWhite.S[11,12] GrantWhite.S textual   speed 0.3361369 0.10066944  
52   GrantWhite.M[1,1] GrantWhite.M       1      x1 4.9298873 0.09536377  
53   GrantWhite.M[1,2] GrantWhite.M       1      x2 6.2000061 0.09196660  
54   GrantWhite.M[1,3] GrantWhite.M       1      x3 1.9956930 0.08603836  
55   GrantWhite.M[1,4] GrantWhite.M       1      x4 3.3172440 0.09311495  
56   GrantWhite.M[1,5] GrantWhite.M       1      x5 4.7120717 0.09619155  
57   GrantWhite.M[1,6] GrantWhite.M       1      x6 2.4689690 0.09396047  
58   GrantWhite.M[1,7] GrantWhite.M       1      x7 3.9208415 0.08557322  
59   GrantWhite.M[1,8] GrantWhite.M       1      x8 5.4882787 0.08687680  
60   GrantWhite.M[1,9] GrantWhite.M       1      x9 5.3272087 0.08513923  

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-01-28 16:29:10 
Wall clock time: 0.6064961 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 $.