15  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.0472840 0.14082337 !
2                 v_x2    Pasteur.A      x2  visual 0.4123827 0.12320901 !
3                 v_x3    Pasteur.A      x3  visual 0.5969253 0.11495805  
4                 t_x4    Pasteur.A      x4 textual 0.9456720 0.07956284 !
5                 t_x5    Pasteur.A      x5 textual 1.1190457 0.08866266  
6                 t_x6    Pasteur.A      x6 textual 0.8274306 0.06799162 !
7                 s_x7    Pasteur.A      x7   speed 0.5913139 0.10752578 !
8                 s_x8    Pasteur.A      x8   speed 0.6650350 0.10307734  
9                 s_x9    Pasteur.A      x9   speed 0.5451850 0.10425452 !
10                  e1    Pasteur.S      x1      x1 0.2984118 0.25364265 !
11                  e2    Pasteur.S      x2      x2 1.3336825 0.16414237 !
12                  e3    Pasteur.S      x3      x3 0.9894648 0.14395579  
13                  e4    Pasteur.S      x4      x4 0.4253789 0.07039741 !
14                  e5    Pasteur.S      x5      x5 0.4556358 0.08545113  
15                  e6    Pasteur.S      x6      x6 0.2898068 0.05137137  
16                  e7    Pasteur.S      x7      x7 0.8202761 0.12599674  
17                  e8    Pasteur.S      x8      x8 0.5097221 0.11763880  
18                  e9    Pasteur.S      x9      x9 0.6803423 0.11197828 !
19                 v_t    Pasteur.S  visual textual 0.4837478 0.07971572  
20                 v_s    Pasteur.S  visual   speed 0.2989486 0.12071666 !
21                 t_s    Pasteur.S textual   speed 0.3251399 0.10215046 !
22      Pasteur.M[1,1]    Pasteur.M       1      x1 4.9412359 0.09457126  
23      Pasteur.M[1,2]    Pasteur.M       1      x2 5.9839737 0.09818034  
24      Pasteur.M[1,3]    Pasteur.M       1      x3 2.4871772 0.09288073  
25      Pasteur.M[1,4]    Pasteur.M       1      x4 2.8226498 0.09197555  
26      Pasteur.M[1,5]    Pasteur.M       1      x5 3.9951899 0.10463331  
27      Pasteur.M[1,6]    Pasteur.M       1      x6 1.9221609 0.07903474  
28      Pasteur.M[1,7]    Pasteur.M       1      x7 4.4322746 0.08660001  
29      Pasteur.M[1,8]    Pasteur.M       1      x8 5.5631410 0.07811876  
30      Pasteur.M[1,9]    Pasteur.M       1      x9 5.4177337 0.07916110  
31  GrantWhite.A[1,10] GrantWhite.A      x1  visual 0.7770105 0.10597825  
32  GrantWhite.A[2,10] GrantWhite.A      x2  visual 0.5720111 0.10263681 !
33  GrantWhite.A[3,10] GrantWhite.A      x3  visual 0.7185792 0.09609911  
34  GrantWhite.A[4,11] GrantWhite.A      x4 textual 0.9705191 0.07858211  
35  GrantWhite.A[5,11] GrantWhite.A      x5 textual 0.9606188 0.08265419 !
36  GrantWhite.A[6,11] GrantWhite.A      x6 textual 0.9349367 0.08083586 !
37  GrantWhite.A[7,12] GrantWhite.A      x7   speed 0.6791978 0.08926541  
38  GrantWhite.A[8,12] GrantWhite.A      x8   speed 0.8326002 0.09476837 !
39  GrantWhite.A[9,12] GrantWhite.A      x9   speed 0.7184970 0.09689599 !
40   GrantWhite.S[1,1] GrantWhite.S      x1      x1 0.7148990 0.13128980  
41   GrantWhite.S[2,2] GrantWhite.S      x2      x2 0.8991877 0.12387867  
42   GrantWhite.S[3,3] GrantWhite.S      x3      x3 0.5570079 0.10836054  
43   GrantWhite.S[4,4] GrantWhite.S      x4      x4 0.3153073 0.06484767  
44   GrantWhite.S[5,5] GrantWhite.S      x5      x5 0.4188848 0.07229325  
45   GrantWhite.S[6,6] GrantWhite.S      x6      x6 0.4060282 0.06920464 !
46   GrantWhite.S[7,7] GrantWhite.S      x7      x7 0.6004901 0.09539707  
47   GrantWhite.S[8,8] GrantWhite.S      x8      x8 0.4011655 0.11323841  
48   GrantWhite.S[9,9] GrantWhite.S      x9      x9 0.5348128 0.10972797 !
49 GrantWhite.S[10,11] GrantWhite.S  visual textual 0.5406670 0.08524652  
50 GrantWhite.S[10,12] GrantWhite.S  visual   speed 0.5233311 0.10912686  
51 GrantWhite.S[11,12] GrantWhite.S textual   speed 0.3361186 0.10067590  
52   GrantWhite.M[1,1] GrantWhite.M       1      x1 4.9298843 0.09536280  
53   GrantWhite.M[1,2] GrantWhite.M       1      x2 6.1999993 0.09196628  
54   GrantWhite.M[1,3] GrantWhite.M       1      x3 1.9956863 0.08603767  
55   GrantWhite.M[1,4] GrantWhite.M       1      x4 3.3172410 0.09311496  
56   GrantWhite.M[1,5] GrantWhite.M       1      x5 4.7120693 0.09619188  
57   GrantWhite.M[1,6] GrantWhite.M       1      x6 2.4689634 0.09395996  
58   GrantWhite.M[1,7] GrantWhite.M       1      x7 3.9208360 0.08557300  
59   GrantWhite.M[1,8] GrantWhite.M       1      x8 5.4882745 0.08687622  
60   GrantWhite.M[1,9] GrantWhite.M       1      x9 5.3271974 0.08513871  

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-07 09:54:33 
Wall clock time: 1.083042 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 $.