Working with fitted nlpsem models

Newly fitted models retain the specification used to construct them. You can inspect and plot the returned object without repeating its outcome names, curve family, or selected waves. The underlying OpenMx model remains available in fit@mxOutput.

Fit a small model

This example generates five measurements for 80 people with a random intercept and slope. Times are measured from the first occasion. The example is deliberately small so that it can be fitted when this article is built.

set.seed(20261024)
n <- 80L
times <- 0:4
intercept <- rnorm(n, mean = 10, sd = 2)
slope <- rnorm(n, mean = 1.5, sd = 0.3)
dat <- data.frame(ID = seq_len(n))
for (j in seq_along(times)) {
  dat[[paste0("T", j)]] <- times[j]
  dat[[paste0("Y", j)]] <- intercept + slope * times[j] + rnorm(n, sd = 0.5)
}

fit <- getLGCM(
  dat, t_var = "T", y_var = "Y", records = 1:5,
  curveFun = "LIN", intrinsic = FALSE, paramOut = TRUE
)

Inspect the fitted object

The compact print method gives an overview. The summary method delegates to OpenMx, so its convergence and fit diagnostics remain available.

print(fit)
#> nlpsem fit: LGCM, Linear Functional Form
#> Fitted with: getLGCM()
#> Observations: 80
#> Free parameters: 6
#> -2 log-likelihood: 1018
#> Optimizer status: 0
summary(fit)
#> Summary of LGCM, Linear Functional Form 
#>  
#> free parameters:
#>          name matrix  row  col    Estimate  Std.Error A
#> 1 Y_residuals      S   Y1   Y1 0.226498519 0.02067648  
#> 2     Y_psi00      S eta0 eta0 3.992536537 0.65290505  
#> 3     Y_psi01      S eta0 eta1 0.000168311 0.07117458  
#> 4     Y_psi11      S eta1 eta1 0.074688813 0.01552890  
#> 5    Y_mueta0      M    1 eta0 9.893848969 0.22716833  
#> 6    Y_mueta1      M    1 eta1 1.549372372 0.03488170  
#> 
#> Model Statistics: 
#>                |  Parameters  |  Degrees of Freedom  |  Fit (-2lnL units)
#>        Model:              6                    394              1018.368
#>    Saturated:             20                    380                    NA
#> Independence:             10                    390                    NA
#> Number of observations/statistics: 80/400
#> 
#> Information Criteria: 
#>       |  df Penalty  |  Parameters Penalty  |  Sample-Size Adjusted
#> AIC:       230.3684               1030.368                 1031.519
#> BIC:      -708.1501               1044.661                 1025.740
#> To get additional fit indices, see help(mxRefModels)
#> timestamp: 2026-11-01 23:57:15 
#> Wall clock time: 0.06041312 secs 
#> optimizer:  SLSQP 
#> OpenMx version number: 2.22.11 
#> Need help?  See help(mxSummary)

Optimizer completion does not establish statistical admissibility. Inspect stored covariance matrices, uncertainty information and component sizes without refitting:

diagnostics <- getDiagnostics(fit)
diagnostics
#> $optimizer
#> $optimizer$status
#> [1] 0
#> 
#> $optimizer$status_zero
#> [1] TRUE
#> 
#> $optimizer$finite_likelihood
#> [1] TRUE
#> 
#> 
#> $covariance
#>                          model matrix                     block dimension
#> 1 LGCM, Linear Functional Form      S          RAM disturbances         7
#> 2 LGCM, Linear Functional Form      S       Latent disturbances         2
#> 3 LGCM, Linear Functional Form      S        Manifest residuals         5
#> 4 LGCM, Linear Functional Form Y_psi0 Stored covariance algebra         2
#>              status minimum_variance minimum_eigenvalue
#> 1 positive_definite       0.07468881         0.07468881
#> 2 positive_definite       0.07468881         0.07468881
#> 3 positive_definite       0.22649852         0.22649852
#> 4 positive_definite       0.07468881         0.07468881
#>   scaled_minimum_eigenvalue negative_variances
#> 1                 0.9996918                   
#> 2                 0.9996918                   
#> 3                 1.0000000                   
#> 4                 0.9996918                   
#>                                                     detail
#> 1 Stored matrix; diagonal-scaled eigenvalue classification
#> 2 Stored matrix; diagonal-scaled eigenvalue classification
#> 3 Stored matrix; diagonal-scaled eigenvalue classification
#> 4 Stored matrix; diagonal-scaled eigenvalue classification
#> 
#> $cross_covariances
#> [1] model            matrix           rows             columns         
#> [5] row_variables    column_variables status           detail          
#> <0 rows> (or 0-length row.names)
#> 
#> $hessian
#> $hessian$dimension
#> [1] 6
#> 
#> $hessian$status
#> [1] "positive_definite"
#> 
#> $hessian$minimum_diagonal
#> [1] 4.741344
#> 
#> $hessian$minimum_eigenvalue
#> [1] 4.691115
#> 
#> $hessian$scaled_minimum_eigenvalue
#> [1] 0.7983534
#> 
#> $hessian$negative_diagonals
#> [1] ""
#> 
#> $hessian$detail
#> [1] "Stored matrix; diagonal-scaled eigenvalue classification"
#> 
#> $hessian$source
#> [1] "calculatedHessian"
#> 
#> $hessian$openmx_info_definite
#> [1] TRUE
#> 
#> 
#> $standard_errors
#> $standard_errors$status
#> [1] "no_issues_detected"
#> 
#> $standard_errors$expected
#> [1] 6
#> 
#> $standard_errors$available
#> [1] 6
#> 
#> $standard_errors$nonfinite
#> [1] 0
#> 
#> $standard_errors$nonpositive
#> [1] 0
#> 
#> 
#> $component_sizes
#> $component_sizes$status
#> [1] "not_applicable"
#> 
#> $component_sizes$table
#> data frame with 0 columns and 0 rows
#> 
#> $component_sizes$detail
#> [1] "Not a recognized nlpsem mixture"
#> 
#> 
#> $issues
#> character(0)
#> 
#> $repeated_starts
#> [1] "not_assessed"
#> 
#> $limitations
#> [1] "No refitting or statistical acceptance is performed"                                                      
#> [2] "Definition-variable covariances and other unsupported expectations are not fully checked"                 
#> [3] "Identification, global optimality, repeated-start stability, and interval calibration are not established"
#> [4] "Stored outputs can be stale if a fitted model was manually modified"

A materially negative variance or indefinite latent covariance requires investigation. Small boundary eigenvalues need scale-aware interpretation. Missing Hessian or standard error information means that check is unavailable. These checks do not establish global optimality: repeat fitting with defensible alternative starts and compare admissible solutions. Keep the seed, optimizer and package versions with the analysis.

The coefficient method returns the named free parameters at full precision. printTable(fit) displays the package’s estimate table, including derived quantities when that model supplies them; these serve different purposes.

coef(fit)
#> Y_residuals     Y_psi00     Y_psi01     Y_psi11    Y_mueta0    Y_mueta1 
#> 0.226498519 3.992536537 0.000168311 0.074688813 9.893848969 1.549372372
logLik(fit)
#> 'log Lik.' -509.1842 (df=6)
printTable(fit)
#> 
#> Point Estimates and Standard Errors:
#>        Name    Estimate         SE
#> 1    mueta0 9.893848969 0.22716833
#> 2    mueta1 1.549372372 0.03488170
#> 3     psi00 3.992536537 0.65290505
#> 4     psi01 0.000168311 0.07117458
#> 5     psi11 0.074688813 0.01552890
#> 6 residuals 0.226498519 0.02067648

Use getSpecification() to inspect the recorded modeling choices and fitting provenance. This is useful when returning to a saved analysis or explaining how a figure was constructed.

provenance <- getSpecification(fit)
names(provenance)
#> [1] "schema_version" "call"           "function_name"  "specification" 
#> [5] "fitting"        "data"           "versions"       "diagnostics"
provenance$specification
#> $t_var
#> [1] "T"
#> 
#> $y_var
#> [1] "Y"
#> 
#> $curveFun
#> [1] "LIN"
#> 
#> $intrinsic
#> [1] FALSE
#> 
#> $records
#> [1] 1 2 3 4 5
#> 
#> $growth_TIC
#> NULL
#> 
#> $TVC
#> NULL
#> 
#> $decompose
#> NULL
#> 
#> $x_type
#> NULL
#> 
#> $x_var
#> NULL
#> 
#> $m_var
#> NULL
#> 
#> $grp_var
#> NULL
#> 
#> $cluster_TIC
#> NULL
#> 
#> $sub_Model
#> [1] "LGCM"
#> 
#> $y_model
#> [1] "LGCM"
#> 
#> $container
#> [1] "single"
#> 
#> $nClass
#> NULL
provenance$versions
#> $R
#> [1] "R version 4.6.2 (2026-10-29)"
#> 
#> $OpenMx
#> [1] "2.22.11"
#> 
#> $nlpsem
#> [1] "1.0.0"

The full object also retains resolved starting values, fitting settings, and available RNG state. Save the original data together with the fitted object; provenance records the specification but is not an automatically replayable call.

Plot without repeating the specification

The plotting method reads the recorded model specification. Supply only presentation choices such as the time label and outcome title.

plot(fit, xstarts = 0, xlab = "Years since baseline", outcome = "Simulated outcome")
#> figOutput Object
#> --------------------
#> Trajectories: 1 
#> Figure 1:

The simulated mean outcome increases from about 10 at baseline to 16 after four years. The fitted linear mean closely follows the smoothed observed mean, with pointwise 95 percent confidence limits on either side.

The intervals describe uncertainty in the model-implied mean at each plotted time. They are pointwise intervals, not prediction intervals for individual people or a simultaneous confidence band for the entire curve.

For further customization, request the plotting object without drawing it:

figures <- plot(fit, draw = FALSE, xstarts = 0, xlab = "Years since baseline",
                outcome = "Simulated outcome")
class(figures)
#> [1] "figOutput"
#> attr(,"package")
#> [1] "nlpsem"

Its figures slot contains the ggplot objects. The layout follows getFigure(): one entry per outcome, with growth-status or change/rate plots within that entry.

Check a model on simulated data

getSimData() generates wide-format data from a model with known parameter values. Each data set carries those values as the attribute truth, retaining the generating parameterization and time origin. This example generates six measurements for 500 people from a quadratic latent growth curve model with the default parameter values.

sim <- getSimData(n = 500, curveFun = "QUAD", nT = 6, seed = 20261027)
truth <- attr(sim, "truth")
sim_fit <- getLGCM(
  sim, t_var = "T", y_var = "Y", records = 1:6,
  curveFun = "QUAD", intrinsic = FALSE, paramOut = TRUE
)
estimates <- sim_fit@Estimates[match(names(truth), sim_fit@Estimates$Name), ]
data.frame(truth = truth, estimate = round(estimates$Estimate, 3), SE = round(estimates$SE, 3))
#>           truth estimate    SE
#> mueta0    50.00   50.171 0.220
#> mueta1    20.00   20.024 0.058
#> mueta2    -1.00   -0.979 0.014
#> psi00     25.00   23.321 1.526
#> psi01      1.50    1.556 0.287
#> psi02      0.45    0.343 0.073
#> psi11      1.00    0.945 0.108
#> psi12      0.09    0.088 0.019
#> psi22      0.09    0.076 0.007
#> residuals  1.00    0.980 0.036

Equal parameter names do not always mean equal parameters. For example, a quadratic growth curve’s eta0 is its level at time zero, while an LCSM’s eta0 is its level at the first fitted occasion. For quadratic and nonparametric data, truth_lcsm supplies the parameter targets at the first generated occasion:

shifted <- getSimData(n = 30, curveFun = "QUAD", times = 5:14, jitter = 0, seed = 1)
cbind(LGCM = attr(shifted, "truth")[c("mueta0", "psi00")],
      LCSM = attr(shifted, "truth_lcsm")[c("mueta0", "psi00")])
#>        LGCM   LCSM
#> mueta0   50 125.00
#> psi00    25 166.25
attr(shifted, "truth_info")$first_observation_time
#> [1] 5

The quadratic transformation includes the mean, covariance and covariate effects; the initial mean and variance above are 125 and 166.25 for the LCSM, versus the time-zero values 50 and 25. The nonparametric generator already starts at the first observation. These targets cover all generated records; a later selected baseline needs reparameterization, potentially for each person if that time is jittered. truth_info records the origin, reference records and applicable limits.

truth_lcsm is unavailable for EXP and JB: their midpoint approximation, and a random ratio’s nonlinear transformation, preclude treating a simple conversion as an exact recovery target. Direct comparisons with the generating truth need a zero first fitted time for every person and the approximation qualifications in ?getSimData. For TVC models, transformed factor targets alone do not establish equivalence of the full growth-curve and change-score data-generating models.

The data also carry each person’s growth factors (growth_factors) and the full generating values (settings). Supply params to change the generating values, and a list of parameter sets with prop to generate latent classes. The same function generates data for multivariate, time-varying covariate and mediation models:

bivariate <- getSimData(n = 200, sub_Model = "MGM", curveFun = "LIN", seed = 1)
with_tvc <- getSimData(n = 200, sub_Model = "TVC", curveFun = "LIN", decompose = 1, seed = 1)
mediation <- getSimData(n = 200, sub_Model = "MED", curveFun = "LIN", x_type = "baseline", seed = 1)
head(attr(mediation, "truth"))
#>       muX     phi11   alphaM0   alphaM1 psiM0M0_r psiM0M1_r 
#>       0.0       1.0      50.0       5.0      25.0       1.5

The matching fit for each is getMGM(), getTVCmodel() and getMediation() with the same functional form; ?getSimData lists the parameter values each model takes.

Reuse existing analyses

The fitting functions retain their existing arguments and output slots. Calls that work directly with OpenMx models, such as the following, remain available:

getSummary(model_list = list(fit@mxOutput))
#>    Model No_Params    -2ll     AIC     BIC Y_residuals
#> 1 Model1         6 1018.37 1030.37 1044.66      0.2265

Objects saved by older nlpsem versions may not contain specification metadata. Their OpenMx results can still be inspected, but structural choices must be supplied when plotting. For example, for an older univariate linear fit:

plot(
  old_fit, sub_Model = "LGCM", y_var = "Y", curveFun = "LIN",
  t_var = "T", records = 1:5, xstarts = 0,
  xlab = "Years since baseline", outcome = "Outcome"
)

Use the specification from the original analysis. Do not infer the selected waves or curve family solely from the appearance of a plotted trajectory. See the model-specific vignettes for time-varying covariates, multiple outcomes, mediation, groups, and mixtures.