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.0473319 0.14080129  
2                 v_x2    Pasteur.A      x2  visual 0.4123505 0.12320629 !
3                 v_x3    Pasteur.A      x3  visual 0.5969210 0.11494721 !
4                 t_x4    Pasteur.A      x4 textual 0.9456798 0.07956193  
5                 t_x5    Pasteur.A      x5 textual 1.1190455 0.08866297 !
6                 t_x6    Pasteur.A      x6 textual 0.8274283 0.06799127  
7                 s_x7    Pasteur.A      x7   speed 0.5912865 0.10751799 !
8                 s_x8    Pasteur.A      x8   speed 0.6650374 0.10307401 !
9                 s_x9    Pasteur.A      x9   speed 0.5451869 0.10425742  
10                  e1    Pasteur.S      x1      x1 0.2983277 0.25360187 !
11                  e2    Pasteur.S      x2      x2 1.3337081 0.16413659 !
12                  e3    Pasteur.S      x3      x3 0.9894888 0.14391201  
13                  e4    Pasteur.S      x4      x4 0.4253778 0.07039732  
14                  e5    Pasteur.S      x5      x5 0.4556400 0.08544950  
15                  e6    Pasteur.S      x6      x6 0.2898116 0.05137054  
16                  e7    Pasteur.S      x7      x7 0.8202946 0.12598896 !
17                  e8    Pasteur.S      x8      x8 0.5097193 0.11763857  
18                  e9    Pasteur.S      x9      x9 0.6803254 0.11197730  
19                 v_t    Pasteur.S  visual textual 0.4837488 0.07971762  
20                 v_s    Pasteur.S  visual   speed 0.2989587 0.12072552 !
21                 t_s    Pasteur.S textual   speed 0.3251488 0.10214663  
22      Pasteur.M[1,1]    Pasteur.M       1      x1 4.9412463 0.09457206  
23      Pasteur.M[1,2]    Pasteur.M       1      x2 5.9839743 0.09818037  
24      Pasteur.M[1,3]    Pasteur.M       1      x3 2.4871850 0.09288150  
25      Pasteur.M[1,4]    Pasteur.M       1      x4 2.8226509 0.09197633  
26      Pasteur.M[1,5]    Pasteur.M       1      x5 3.9951917 0.10463394  
27      Pasteur.M[1,6]    Pasteur.M       1      x6 1.9221622 0.07903516  
28      Pasteur.M[1,7]    Pasteur.M       1      x7 4.4322693 0.08659949  
29      Pasteur.M[1,8]    Pasteur.M       1      x8 5.5631413 0.07811880  
30      Pasteur.M[1,9]    Pasteur.M       1      x9 5.4177337 0.07916054  
31  GrantWhite.A[1,10] GrantWhite.A      x1  visual 0.7770116 0.10597038  
32  GrantWhite.A[2,10] GrantWhite.A      x2  visual 0.5719967 0.10264262 !
33  GrantWhite.A[3,10] GrantWhite.A      x3  visual 0.7185799 0.09609481  
34  GrantWhite.A[4,11] GrantWhite.A      x4 textual 0.9705170 0.07857832  
35  GrantWhite.A[5,11] GrantWhite.A      x5 textual 0.9606099 0.08265008  
36  GrantWhite.A[6,11] GrantWhite.A      x6 textual 0.9349369 0.08083283  
37  GrantWhite.A[7,12] GrantWhite.A      x7   speed 0.6791950 0.08926842  
38  GrantWhite.A[8,12] GrantWhite.A      x8   speed 0.8325839 0.09476770  
39  GrantWhite.A[9,12] GrantWhite.A      x9   speed 0.7185080 0.09688822  
40   GrantWhite.S[1,1] GrantWhite.S      x1      x1 0.7148994 0.13128231  
41   GrantWhite.S[2,2] GrantWhite.S      x2      x2 0.8991915 0.12388958  
42   GrantWhite.S[3,3] GrantWhite.S      x3      x3 0.5570062 0.10835464  
43   GrantWhite.S[4,4] GrantWhite.S      x4      x4 0.3153058 0.06484840  
44   GrantWhite.S[5,5] GrantWhite.S      x5      x5 0.4188859 0.07229452 !
45   GrantWhite.S[6,6] GrantWhite.S      x6      x6 0.4060290 0.06920622  
46   GrantWhite.S[7,7] GrantWhite.S      x7      x7 0.6004919 0.09539767  
47   GrantWhite.S[8,8] GrantWhite.S      x8      x8 0.4011815 0.11323174  
48   GrantWhite.S[9,9] GrantWhite.S      x9      x9 0.5347947 0.10971531  
49 GrantWhite.S[10,11] GrantWhite.S  visual textual 0.5406631 0.08524357 !
50 GrantWhite.S[10,12] GrantWhite.S  visual   speed 0.5233342 0.10912285 !
51 GrantWhite.S[11,12] GrantWhite.S textual   speed 0.3361264 0.10066601  
52   GrantWhite.M[1,1] GrantWhite.M       1      x1 4.9298821 0.09536311  
53   GrantWhite.M[1,2] GrantWhite.M       1      x2 6.1999967 0.09196595  
54   GrantWhite.M[1,3] GrantWhite.M       1      x3 1.9956878 0.08603773  
55   GrantWhite.M[1,4] GrantWhite.M       1      x4 3.3172374 0.09311495  
56   GrantWhite.M[1,5] GrantWhite.M       1      x5 4.7120677 0.09619154  
57   GrantWhite.M[1,6] GrantWhite.M       1      x6 2.4689630 0.09396016  
58   GrantWhite.M[1,7] GrantWhite.M       1      x7 3.9208410 0.08557310  
59   GrantWhite.M[1,8] GrantWhite.M       1      x8 5.4882752 0.08687608  
60   GrantWhite.M[1,9] GrantWhite.M       1      x9 5.3272029 0.08513890  

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: 2025-12-18 14:48:38 
Wall clock time: 0.5958929 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 $.