magmaanlab: methods-development R bindings

magmaanlab is the compiled R package over the magmaan C++ core, for methods development and research. The opinionated one-call package for ordinary users is magmaan in ../r-magmaan/, which builds on this one; see the two-package vision.

The friendly helpers compose a staged SEM workflow, while C++-shaped primitives are available through the magmaan_core object for interactive methods work.

Convenience helpers are limited to R-side composition:

Low-level functions such as compat_lavaan_lavaanify(), model_matrix_rep(), estimate_fit(), estimate_*(), data_sample_stats_from_raw(), data_ordinal_stats_from_raw(), data_mixed_ordinal_stats_from_raw(), and the inference_* / robust_* families are available as magmaan_core$... entries so the C++ architecture is still directly inspectable from R without flooding ordinary tab completion. Frontier structured-Gamma helpers are similarly explicit: magmaan_core$estimate_structured_gamma() returns the raw MI4 Gamma matrix, and magmaan_core$estimate_structured_gamma_weight() returns its direct inverse for continuous WLS when it is already positive definite. Older spellings such as lavaan_lavaanify(), fit_fit(), fit_*_impl(), method-specific ordinal data builders, and infer_* remain available as compatibility aliases during exploration, but they are no longer listed in attr(magmaan_core, "groups") or the compact magmaan_core printout. Model-dependent post-fit helpers expose primitive-shaped entry points such as magmaan_core$inference_vcov_partable(info, partable), magmaan_core$inference_z_test_theta(theta, se), magmaan_core$inference_wald_test_theta(theta, R, vcov), magmaan_core$inference_nt_moment_quadratic_sample(sample_stats, implied), magmaan_core$robust_build_u_factor_parts(partable, sample_stats, theta), and magmaan_core$robust_reduced_gamma_sample_zc(...) / magmaan_core$robust_reduced_gamma_sample_gamma(...), magmaan_core$robust_test_moments_both_breads_zc(...) / magmaan_core$robust_test_moments_both_breads_gamma(...), and magmaan_core$robust_se*_parts(...). Fit-list calls remain available in magmaan_core, with explicit *_fit aliases for scripts that prefer adapter-style names.

Reusable model, data and weights

For repeated estimation, use the native prepared interface:

model <- prepare_model("f =~ x1 + x2 + x3 + x4")
data <- prepare_data(model, df)
fit <- estimate(model, data, estimator = "ML")

# A new replication reuses the model, but prepares its own data and weights.
data2 <- prepare_data(model, df2)
weight2 <- prepare_weight(data2, "DWLS")
fit2 <- estimate(model, data2, weight = weight2)

prepare_model() retains the native model triple and matrix representation. prepare_data() builds per-dataset moments or a FIML missingness pack, without estimation weights. prepare_weight() owns the weight separately. estimate() refreshes data-dependent starts, fits and audits; inference remains explicit. Returned fits use the existing fit-list interface.

Data kind Prepared estimators
"moments" (default for continuous models) ML, ULS, GLS, WLS, DWLS
"raw" (requires a model with meanstructure = TRUE) FIML
"ordinal" (all observed variables ordered) ULS, DWLS, WLS; delta/theta
"mixed" (some observed variables ordered) DWLS, WLS; delta/theta

Continuous data can also be supplied as list(S = ..., nobs = ..., mean = ...) with N-divisor covariances. Empirical continuous weights need raw observations; otherwise supply W explicitly. The prepared weight exposes $W for inspection and for post-fit functions taking an explicit weight.

Fixed observed covariates are unsupported because conditional moments (conditional.x) are not implemented. Such models fail explicitly. Setting fixed_x = FALSE specifies a joint random-covariate model and changes the statistical model; it does not request conditional estimation.

For categorical models, declare the category schema once:

model <- prepare_model("f =~ x1 + x2 + x3 + x4",
                       ordered = names(df), prototype = df)
data <- prepare_data(model, df)
weight <- prepare_weight(data, "DWLS", full = FALSE)
fit <- estimate(model, data, weight = weight)

The prototype contributes category labels/order only. It contributes no empirical thresholds or starting values. Each dataset must contain all declared categories; changed levels, missing model variables and incompatible groups fail explicitly. Declare group labels in model_spec()/prepare_model(); columns are matched by name. Use missing = "listwise" explicitly for incomplete moment data.

For categorical weights, full = TRUE (the default) retains full Gamma and influence ingredients for existing post-fit inference; DWLS does not invert the full Gamma. full = FALSE builds only the DWLS diagonal. The resulting fit lacks full-Gamma inference ingredients. These can be requested separately, without refitting: inference_weight <- prepare_weight(data, "DWLS", full = TRUE) and then, for example, magmaan_core$robust_ordinal(fit, inference_weight$stats). ULS can similarly use prepare_weight(data, "ULS", full = TRUE) when full-Gamma inference is wanted. Weight preparation currently calls the existing categorical builders, which recompute stage-one moments; that cost is outside estimation but remains an optimization opportunity. Measure model, data, weight, fit and inference stages separately.

Handles are immutable and process-local: rebuild them in each worker, and after reading serialized objects. Reusing data across models requires identical observable order, category/group schema and mean-structure convention. A weight is tied to its dataset and cannot silently be reused for another replication.

Existing model_spec(), fit_model(), fit_* and magmaan_core$estimate_* calls remain supported without deprecation warnings. model_spec() alone retains an R partable, not a compiled native model. Specialized ML2S, two-level, FC-SEM, SAM and frontier paths still use their existing entry points. Deprecation and removal wait for coverage and caller migration; see the interface audit and rollout status.

Sample-moment data

Complete-data ML/ULS/GLS/WLS fit wrappers accept sample moments as list(S = , nobs = , mean = ):

FMG robust p-values

FMG single-model goodness-of-fit tests are exposed as a post-fit inference family:

Ordinal LS boundary

Ordinal support is intentionally narrow and mirrors the C++ ordinal LS path:

Continuous SNLLS contract

Use magmaan_core$fit_uls_snlls(model, data), fit_gls_snlls(model, data), or fit_wls_snlls(model, data, W) for fixed-weight separable fits. These paths are unbounded: supplied bounds are rejected, including variance-bound presets. Choose the corresponding ordinary LS fit when bounds are required. NULL or the friendly wrapper's bounds = "none" requests unbounded fitting. Covariance admissibility is reported separately; profiling does not impose PSD.

fit$diagnostics$geometric_stationarity recomputes stationarity in the full model coordinates, including the eliminated linear parameters. It is additive and does not replace fit$audit, which retains the optimizer's driven-coordinate verdict. GP uses Kaufman's approximate residual Jacobian: scalar gradients are exact locally at fixed rank with accurate inner solves, while residual-based backends use approximate Gauss–Newton curvature.

Sharing NTML work across tests

For complete-data ML, use one inference dataset across the fitted models. The native objects retain the existing NTML U-factor setup, casewise contributions, information and each test's reduced covariance/spectrum:

d <- prepare_inference_data(fit1, data)
g0 <- prepare_inference(fit0, d)
g1 <- prepare_inference(fit1, d)
h <- prepare_hypothesis(g0, g1)

# Global score and ML GOF at one fit:
qs <- inference_quadratic(g0, "score")
ql <- inference_quadratic(g0, "lr")
# Nested score and exact Satorra–2000 LR:
ns <- inference_quadratic(h, "score")
nl <- inference_quadratic(h, "lr")
calibrate_quadratic(ns, c("sb", "peba4"))
calibrate_quadratic(nl, c("sb", "peba4"))

V <- inference_covariance(g0, robust = TRUE) # reusable by wald_test()
inference_reuse(g0)                         # construction counts

Repeated calls reuse the native results, including spectra. SB-only calibration uses the trace. storage = "auto" caches casewise moment contributions up to 64 MiB, then uses tiled projections. The large-N global path accumulates score and GOF reductions together; the smaller row-space spectrum is used when appropriate. Neither empirical Gamma nor full U is required. Tiled storage may revisit raw data for distinct hypotheses; an explicit unbiased GOF request can materialize casewise contributions for the existing correction.

This path currently supports structured expected-information geometry, affine exact parameter nesting, interior fits and random X. Score uses likelihood contributions at H0; LR uses its own H1-anchored reduction. Their finite-sample spectra remain distinct. Compatible prepared fits also feed the existing GOF, expected-information, expected sandwich-SE and exact empirical streaming LR wrappers. Other conventions, FIML and ML2S retain their existing interfaces. See examples/inference_reuse.R for parity and reuse checks.

Reusable score and inference objects

Scores are available without running a score test. For ML/FIML, prepare an immutable inference snapshot once and choose the subsequent work explicitly:

ctx <- prepare_inference(fit, data) # omit data for FIML/ML2S
s <- scores(ctx, space = "parameter")
c <- score_components(ctx)         # global saturated complement
u <- project_scores(c, retain_rows = TRUE)
e <- score_spectrum(u)             # compute once, reuse below
calibrate_quadratic(e, c("sb", "peba2", "peba4"))
resample_scores(u, n_flips = 999, seed = 7)

score_components(ctx, H1 = unrestricted_model) constructs affine nested ML/FIML ingredients without fitting H1. Neither component construction nor projection computes an exact-mixture p-value. calibrate_quadratic(u, "sb") uses a trace without an eigendecomposition; request "all" explicitly for the exact mixture. score_sandwich(u) is separate, so singular empirical meat can still support mixture calibration. center = TRUE on projection explicitly centers covariance rows globally, not within missingness patterns.

score_components_from_matrices() accepts existing scores, rows, sensitivity, metric and chosen nuisance/test directions for method development. It uses the same C++ projection and downstream consumers as the fit adapters.

NT-ML2S has a separate adapter: its observed Stage-2 score is not the sum of its Stage-1 influence rows. Regularized Stage 1 and non-NT weights remain unsupported. Full casewise likelihood scores and parameter information use the ML/FIML adapters; no universal likelihood-score interpretation is imposed on other estimators.

Information and covariance can also be computed once and reused:

I <- inference_information(ctx, "expected")
V <- parameter_covariance(ctx, I)   # optionally supply a total-scale meat
wald_test(ctx, R, vcov = V, q = target)

These matrices carry snapshot provenance, and using one with a different snapshot is rejected. Supplied matrices and parameter vectors remain available for methods development. Snapshots are process-local and immutable; changes to the source fit require a new snapshot. The prepared fit context is also reused by fmg_tests(ctx), fmg_nested(ctx1, ctx0), and robust_nested_lrt(ctx1, ctx0). Legacy LR/GOF routines retain their own geometry construction; reuse their returned statistic/spectrum explicitly when only the calibration changes:

lr <- robust_nested_lrt(ctx1, ctx0)
r <- quadratic_reference(lr$T_diff, lr$df_diff, lr$eigenvalues)
calibrate_quadratic(r, c("sb", "peba2", "peba4"))

See examples/scores.R for executable parity and ownership checks, and ?scores for the coordinate and normalization contracts.