Documentation and metadata release, prepared in response to the CRAN reviewer’s comments on the 0.6.0 submission. No user-visible behaviour, no function signature, and no computed result changes; the 0.5.0 and 0.6.0 regression suites pass unchanged.
umg_eda_scaffold() is no longer wrapped
in \dontrun{}. It runs under @examplesIf,
conditional on ‘ggplot2’ being installed, which is the convention
already used by umg_ggplot() and
umg_render_dot(), and it sets a seed.Correctness release. The demonstrations prepared for the companion
manuscript (Yu, 2026, “The Unified Model Graph”) exercised 0.5.0 against
lavaan, lme4, mirt, dagitty, and Graphviz and recorded fourteen
behaviours that were wrong, misleading, or cosmetic. The statistical and
export defects are fixed here; each fix is pinned by a regression test
in tests/testthat/test-regressions-060.R. Rendering and
design items (annotation rendering, arrowheads into square vertices,
label overlap, the ESEM rotational constraints, the exogenous-cause
covariances of the MIMIC and formative motifs, and the slope proxy of
the EDA scaffold) are deferred to a later release.
umg_validate() implements the full plate rule. Besides
the existing coherence checks (members declared, parent declared,
nesting acyclic), it now refuses (a) a vertex that belongs to a nested
plate but not to the parent plate (“W5 violated: vertex ‘y’ is in plate
‘occasion’ but not in its parent plate ‘cluster’.”); (b) a
dep or det edge u -> v whose source is
replicated over an index the target is not (plates(u) must be a subset
of plates(v); for example an item parameter pointing at a person-level
ability); (c) a cov edge joining vertices with different
plate membership; and (d) a mix edge from a class vertex c
that selects a parameter used by a random vertex not replicated over the
plates of c. The mix target itself is exempt from (b): it denotes a
K-vector from which one element is selected per copy of c. Previously
0.5.0 accepted all of these.umg_edge() gains aggregate = FALSE. A
deterministic edge marked aggregate = TRUE (a cluster mean
computed from occasion-level observations, say) is exempt from the
index-licensing clause, since an aggregate legitimately collapses an
index. The argument is rejected on non-det edges.umg_from_lavaan(), umg_from_lmer(),
umg_from_mirt(), and the rest) was checked under the new
rule; all of them validate, so no builder had to change.umg_count_parameters(meanstructure = TRUE) counted one
free mean per random vertex, so a linear growth model over four
occasions returned df = 1 where lavaan returns df = 5. The free means
are now counted as the documentation states: when the vertices carry
mean annotations (as written by umg_from_lavaan() from a
fit with a mean structure) the vertices annotated
mean = free are counted; otherwise the growth-model
convention applies (indicators of latent factors have zero intercepts,
so the free means are the latent source means plus the intercepts of
observed vertices not regressed on a latent vertex). The Demo.growth
model now returns 14 moments, 9 free parameters, df = 5 from both
umg_from_lavaan(fit) and umg_growth(4). A new
argument n_means overrides the automatic count for other
conventions (for a CFA with free observed intercepts and zero latent
means, n_means = p leaves the df unchanged); it is also
accepted by umg_identify().umg_implied_ci() returned each symmetric statement
twice ((x, y | Z) and (y, x | Z)); a one-factor model with four
indicators listed 12 rows for 6 independencies. Each statement is now
listed once in canonical form (x before y in
the model’s vertex order, conditioning set in vertex order), and
print.umg_identification() counts the de-duplicated
set.umg_check_scaling() no longer accepts a constant parent
as a scaling mark: a constant parent fixes the location of a latent
variable, not its scale. via is now one of
"marker loading", "fixed variance",
"marker weight" (a unit-valued incoming weight from an
observed cause, the marker convention for formative composites), or
"none", and a new informational column
location_fixed reports whether a constant parent is
present.umg_formative() now scales the composite: the weight of
the first indicator is fixed to 1 (argument
scaling = "marker", the default; "none"
reproduces the unscaled drawing), so the motif passes the package’s own
scaling check. The default motif with four causes and two outcomes
counts 12 free parameters (previously 13) and df = 9.umg_from_lavaan() (and umg_from_blavaan())
record the ~1 rows of the parameter table as mean
annotations on the vertices (mean = free,
mean = 0, mean = <value>, or
mean = fixed for a mean fixed at a sample value under
fixed.x = TRUE), and umg_to_lavaan() emits
them as intercept lines (v ~ 1, v ~ 0*1,
v ~ <value>*1). A growth model specified with
lavaan::growth() now round-trips through
lavaan::cfa() with identical npar, df, and chi-square
(Demo.growth: 9, 5, 8.069); previously the cfa() refit
silently dropped the mean structure (df = 3). Fits without a mean
structure (the Holzinger-Swineford CFA) produce exactly the same syntax
as before.prior to an existing
annotation instead of skipping vertices that already carried one.umg_to_dot() no longer writes style=curved
on covariance edges, which Graphviz does not support (it warned
“unsupported style curved” on every covariance edge and ignored it); the
edges keep dir=both and constraint=false.umg_to_dot() emits the note “crossed plates rendered by
smallest-enclosing assignment” only when plates actually cross, that is,
when a vertex belongs to two plates neither of which encloses the other.
Nested plates (a random-coefficient model from
umg_from_lmer()) no longer trigger it.umg_to_tikz() never prints a negative zero coordinate
(-0); a coordinate that rounds to zero from below is
written as 0.@importFrom tags for grid and grDevices (one of
which spanned several lines) imported symbols the code never uses
unqualified; they are removed. Both packages remain in
Imports: and are called with ::.inst/CITATION cites the package as version 0.6.0 and
adds the companion manuscript (Yu, H.-T., 2026, When is a diagram a
model? A unified, determinate graphical notation for the statistical
models of psychology; manuscript submitted for publication).Correctness release prepared for the first CRAN submission. An
adversarial code review of 0.4.0 confirmed a set of defects that
R CMD check and the shape-level tests could not catch; all
are fixed here and each fix is pinned by a regression test
(tests/testthat/test-regressions-050.R).
umg_riclpm() now encodes the Hamaker, Kuiper, and
Grasman (2015) RI-CLPM exactly: wave-specific innovation covariances are
drawn at every wave (not only wave 1), observed vertices carry
var = 0 because the within/between decomposition is exact,
and the lagged paths are wave-indexed rather than sharing one label
(which read as an unintended stationarity constraint). The counting rule
now reproduces the textbook 27 free parameters / df = 9 for four
waves.umg_sem(scaling = "variance") fixes the scale of
endogenous factors too, by a unit residual-variance annotation
matching lavaan’s std.lv = TRUE; previously their scale was
fixed by nothing.umg_secondorder() no longer fixes both the general
factor’s variance and its first loading (an over-restriction, not a
scaling choice); the general factor is marker-scaled with a free
variance.umg_mixture() keeps the mixing-proportion parameter
outside the person plate (a population quantity), consistent with
umg_lca(), and rejects unknown plate names
with a clear error.umg_dcm() gains attr_cov (default
TRUE) drawing the attribute associations that standard DCMs
assume, warns when the Q-matrix looks transposed (items-by-attributes,
the GDINA/CDM convention), and documents the attribute-level
collapse.umg_dsep() is rewritten on the moralized ancestral
graph criterion (Lauritzen et al., 1990). The previous implementation
enumerated skeleton paths with a silent 20,000-path cap and returned
wrong verdicts on moderately dense graphs (including a saturated 10-node
network); the new test is exact and linear in the graph size.umg_count_parameters() subtracts any fixed variance
read from a vertex’s distribution annotation, whether the vertex is
latent or observed and independently of marker scaling (previously a
fixed variance was missed whenever a marker loading was also present,
and observed fixed variances were always counted free). The counting
rule now also declares itself inapplicable
(applicable = FALSE, df = NA, with a warning)
for models containing categorical random vertices, where the
covariance-moment count is not a valid check.umg_implied_ci(observed_only = TRUE) now requires the
conditioning set to be observed as well, so the listed statements are
genuinely testable.umg_from_lavaan() reads only the first group of a
multi-group fit (with a message) instead of silently duplicating every
edge once per group; types ordinal indicators as categorical and carries
their threshold counts as annotations; falls back to the fitted value
for parameters fixed with NA ustart (the
fixed.x = TRUE case, which previously rendered a literal
“NA”); and warns about, rather than crashing on, unsupported operators
such as <~.umg_from_mirt() reads the item type from a fitted
object instead of silently assuming "2PL"; an explicit
model argument still overrides.umg_from_qgraph() symmetrises asymmetric (directed)
weights matrices with a warning instead of silently dropping the lower
triangle.umg_from_lmer()/umg_from_brms() warn when
the grouping variable is missing from data instead of
producing an “i = 1, …, 0” plate.umg_network() accepts upper-, lower-, or fully
symmetric adjacency matrices (previously a lower-triangular matrix
yielded an empty graph).umg_to_tikz() no longer corrupts coordinates at or
above ~100 (the previous formatting capped output at four significant
digits).peripheries=2 in
DOT.theme argument is an error, and a style name
("journal", "slide", "cb") is
accepted anywhere a theme is; previously invalid themes were silently
replaced by the default.umg_save(..., backend = "ggplot") writes SVG through
grDevices so the svglite package is not required.\lambda) in the grid, ggplot2, and DOT backends, and
umg_factor()’s default factor label no longer emits an
undefined TeX control sequence in TikZ export.umg_edge() validates label (character
scalar; NULL is normalised to "") and
fixed (non-missing scalar), closing a family of obscure
downstream rendering errors.umg_factor() indexes user-supplied loading labels as
documented (one label per free loading) and accepts a full-length vector
for convenience.umg_theme() drops unknown elements after warning and
rejects NULL element values.umg_dsep() rejects queries where x,
y, or given overlap.Hardening and round-trip release.
umg_to_lavaan(): emit lavaan model syntax from a
diagram, the inverse of umg_from_lavaan(). Directed edges
from a latent to an observed vertex become measurement loadings
(=~); other directed edges become regressions
(~); covariance edges become ~~. The
convention is documented, and ambiguous or unrepresentable edges are
flagged.as.data.frame.umg(): return the vertices or edges of a
diagram as a tidy data frame for inspection, tabulation, and
programmatic editing.summary.umg() / print.summary.umg(): a
structured summary with vertex counts by type, edge counts by kind, the
plate structure, and an identification snapshot.man/*.Rd are now generated from the roxygen
sources, fixing invalid single-backslash TeX escapes in several examples
(e.g. "$\eta_{1i}$") that previously caused a parse error
under R CMD check.print.umg and print.umg_identification now
document their arguments, and the autoplot method uses
delayed S3 registration via @exportS3Method.umg_growth() validates that a supplied
times vector matches the number of occasions instead of
silently recycling or producing NA time scores.umg_mixture() reports an informative error when a
target vertex is absent rather than failing later in validation.umg_from_OpenMx() ignores the diagonal of the RAM
A matrix, avoiding a spurious self-loop (and the cycle
error it would trigger).Substantial expansion toward a CRAN-ready release.
umg_sem(): general SEM assembler from a measurement and
structural specification, with marker or variance scaling.umg_secondorder(): hierarchical (second-order) factor
model.umg_esem(): exploratory SEM with full
cross-loadings.umg_formative(): formative (composite)
measurement.umg_mimic(): multiple-indicator multiple-cause
model.umg_riclpm(): random-intercept cross-lagged panel
model.umg_dcm(): diagnostic classification model with a
Q-matrix.umg_irt() now covers the polytomous
"graded", "PCM", and "GPCM"
families and multidimensional IRT via n_dim.umg_from_blavaan(): Bayesian SEM from blavaan.umg_from_brms(): Bayesian multilevel models from
brms.umg_from_OpenMx(): RAM path models from OpenMx.umg_from_qgraph(): network models from a qgraph object
or weights matrix.umg_from_lavaan() gains a bayesian
argument.umg_identify() and its components
umg_check_scaling(), umg_count_parameters(),
umg_labelswitching(), umg_dsep(), and
umg_implied_ci() read identification-relevant information
off the diagram, including a d-separation reader that treats covariance
edges as latent common causes.umg_ggplot() / autoplot.umg(): a ggplot2
backend.umg_to_dot() / umg_render_dot(): Graphviz
DOT export and DiagrammeR rendering.umg_save() dispatches on
.dot/.gv and accepts
backend = "ggplot".umg_eda_scaffold() now also produces latent-score
distributions, mixture densities, empirical response curves, and
residual correlation heat maps.umg_caterpillar(): shrinkage/caterpillar plot for
cluster estimates.umg_node() gains fill and
annot arguments.