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.0473249 0.14084614 !
2                 v_x2    Pasteur.A      x2  visual 0.4123475 0.12321345 !
3                 v_x3    Pasteur.A      x3  visual 0.5969040 0.11496485  
4                 t_x4    Pasteur.A      x4 textual 0.9456771 0.07956154  
5                 t_x5    Pasteur.A      x5 textual 1.1190491 0.08866112  
6                 t_x6    Pasteur.A      x6 textual 0.8274289 0.06798991  
7                 s_x7    Pasteur.A      x7   speed 0.5913060 0.10752960 !
8                 s_x8    Pasteur.A      x8   speed 0.6650199 0.10308045  
9                 s_x9    Pasteur.A      x9   speed 0.5451898 0.10426517 !
10                  e1    Pasteur.S      x1      x1 0.2983282 0.25369351  
11                  e2    Pasteur.S      x2      x2 1.3337094 0.16414718 !
12                  e3    Pasteur.S      x3      x3 0.9894967 0.14394741 !
13                  e4    Pasteur.S      x4      x4 0.4253815 0.07039979  
14                  e5    Pasteur.S      x5      x5 0.4556383 0.08545164  
15                  e6    Pasteur.S      x6      x6 0.2898083 0.05137110  
16                  e7    Pasteur.S      x7      x7 0.8202878 0.12600411 !
17                  e8    Pasteur.S      x8      x8 0.5097383 0.11764400  
18                  e9    Pasteur.S      x9      x9 0.6803092 0.11198881 !
19                 v_t    Pasteur.S  visual textual 0.4837475 0.07971485  
20                 v_s    Pasteur.S  visual   speed 0.2989614 0.12072929 !
21                 t_s    Pasteur.S textual   speed 0.3251546 0.10214517  
22      Pasteur.M[1,1]    Pasteur.M       1      x1 4.9412361 0.09457088  
23      Pasteur.M[1,2]    Pasteur.M       1      x2 5.9839730 0.09818019  
24      Pasteur.M[1,3]    Pasteur.M       1      x3 2.4871805 0.09288076  
25      Pasteur.M[1,4]    Pasteur.M       1      x4 2.8226481 0.09197549  
26      Pasteur.M[1,5]    Pasteur.M       1      x5 3.9951898 0.10463306  
27      Pasteur.M[1,6]    Pasteur.M       1      x6 1.9221590 0.07903438  
28      Pasteur.M[1,7]    Pasteur.M       1      x7 4.4322763 0.08660000  
29      Pasteur.M[1,8]    Pasteur.M       1      x8 5.5631413 0.07811851  
30      Pasteur.M[1,9]    Pasteur.M       1      x9 5.4177373 0.07915987  
31  GrantWhite.A[1,10] GrantWhite.A      x1  visual 0.7770164 0.10598562  
32  GrantWhite.A[2,10] GrantWhite.A      x2  visual 0.5719986 0.10264504 !
33  GrantWhite.A[3,10] GrantWhite.A      x3  visual 0.7185761 0.09610728  
34  GrantWhite.A[4,11] GrantWhite.A      x4 textual 0.9705225 0.07857382  
35  GrantWhite.A[5,11] GrantWhite.A      x5 textual 0.9606132 0.08264638  
36  GrantWhite.A[6,11] GrantWhite.A      x6 textual 0.9349433 0.08082745  
37  GrantWhite.A[7,12] GrantWhite.A      x7   speed 0.6791947 0.08927107  
38  GrantWhite.A[8,12] GrantWhite.A      x8   speed 0.8325922 0.09477487  
39  GrantWhite.A[9,12] GrantWhite.A      x9   speed 0.7185037 0.09689902  
40   GrantWhite.S[1,1] GrantWhite.S      x1      x1 0.7148923 0.13132176 !
41   GrantWhite.S[2,2] GrantWhite.S      x2      x2 0.8991836 0.12389625 !
42   GrantWhite.S[3,3] GrantWhite.S      x3      x3 0.5570144 0.10838238  
43   GrantWhite.S[4,4] GrantWhite.S      x4      x4 0.3153050 0.06485036  
44   GrantWhite.S[5,5] GrantWhite.S      x5      x5 0.4188909 0.07229841  
45   GrantWhite.S[6,6] GrantWhite.S      x6      x6 0.4060298 0.06920768  
46   GrantWhite.S[7,7] GrantWhite.S      x7      x7 0.6004908 0.09540588  
47   GrantWhite.S[8,8] GrantWhite.S      x8      x8 0.4011699 0.11324854 !
48   GrantWhite.S[9,9] GrantWhite.S      x9      x9 0.5348003 0.10974464  
49 GrantWhite.S[10,11] GrantWhite.S  visual textual 0.5406685 0.08523891  
50 GrantWhite.S[10,12] GrantWhite.S  visual   speed 0.5233355 0.10913294  
51 GrantWhite.S[11,12] GrantWhite.S textual   speed 0.3361244 0.10066771  
52   GrantWhite.M[1,1] GrantWhite.M       1      x1 4.9298828 0.09536179  
53   GrantWhite.M[1,2] GrantWhite.M       1      x2 6.1999994 0.09196504  
54   GrantWhite.M[1,3] GrantWhite.M       1      x3 1.9956876 0.08603690  
55   GrantWhite.M[1,4] GrantWhite.M       1      x4 3.3172428 0.09311358  
56   GrantWhite.M[1,5] GrantWhite.M       1      x5 4.7120698 0.09619017  
57   GrantWhite.M[1,6] GrantWhite.M       1      x6 2.4689651 0.09395900  
58   GrantWhite.M[1,7] GrantWhite.M       1      x7 3.9208345 0.08557215  
59   GrantWhite.M[1,8] GrantWhite.M       1      x8 5.4882740 0.08687494  
60   GrantWhite.M[1,9] GrantWhite.M       1      x9 5.3272041 0.08513783  

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-10-17 18:01:03 
Wall clock time: 1.228974 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 $.